Template literal types
Build string types from patterns: class names like btn-small, sizes like 120px, handler names like onClick, and event names that TypeScript checks for you.
- 01Write a string pattern as a typeBuild string types like
btn-${Size}and${number}px, and read the error when a string doesn't fit. - 02Predict the names a union makesWork out every string a template with unions makes, and change case with
Uppercase,Lowercase, andCapitalize. - 03Type event namesBuild event and handler names from an object's keys, and let TypeScript find the handler's value type from the name.
A string type with a pattern
A shop's buttons get their look from CSS classes. There are two sizes, so there are two classes: btn-small and btn-large. Any other class name, like a typo, gives a button with no style at all. You already know how to list the sizes as a union of literal types. Now you can build the class names from them:
type Size = "small" | "large";type ButtonClass = `btn-${Size}`;// ^?const saveButton: ButtonClass = "btn-smal";console.log(saveButton);Line 1 says a Size is the text "small" or the text "large". Line 2 is the new part. It looks like a JavaScript template literal, with backticks and a ${ } placeholder, but it's a type. It means "the text btn-, followed by a Size". This is called a template literal type. The Mapped types lesson used one to rename keys, `${Key}Label`; this lesson shows what they can do.
The query on line 3 shows what TypeScript made of it: "btn-small" | "btn-large", exactly two strings. Line 4 stores "btn-smal", with an l missing. TypeScript marks it, on purpose here, because it's the bug:
Type '"btn-smal"' is not assignable to type '"btn-small" | "btn-large"'means "this text isn't one of the two class names". "Assignable" means "allowed to be stored there".Did you mean '"btn-small"'?is TypeScript's guess at the fix, because the text is very close to one of the names. This version of the error has the numberts(2820).
Line 5 still prints btn-smal when it runs: the type is a check before the code runs, not a change to it. Now type your own class name for line 4 and watch the list under the code:
type Size = "small" | "large";type ButtonClass = `btn-${Size}`;// ^?const saveButton: ButtonClass = "btn-smal";console.log(saveButton);"btn-smal"1 error: line 4prints btn-smal"btn-smal" is missing an l, so it isn't one of the two names in ButtonClass. TypeScript marks line 4 and even suggests the fix: Did you mean '"btn-small"'?
"btn-small" and "btn-large" have no errors. "btn-medium" gets one, because medium isn't a size. "BTN-SMALL" gets one too: capital letters make a different string. And "small" is missing the fixed text btn- that every name starts with.
Size into the placeholder, one at a time. Two sizes make two names, and ButtonClass is exactly those two strings.An office stamp has some fixed letters and a small wheel you turn to pick a word. The wheel has only a few words carved on it. Whatever you do, every print is the fixed letters followed by one of those words. You could list every possible print before you ever press the stamp on paper.
- In real life: The fixed letters on the stamp, "btn-"
- In TypeScript: The fixed text
btn-in the pattern - In real life: The wheel, which shows only the words carved on it
- In TypeScript: The placeholder, which takes only the members of
Size - In real life: Every print the stamp can make
- In TypeScript: Every string in
ButtonClass:"btn-small"and"btn-large" - In real life: A word that isn't on the wheel can't be printed
- In TypeScript:
"btn-smal"isn't in the type, so it's an error
A template literal type is a string type built from a pattern. You write it like a template literal, with types in the placeholders: `btn-${Size}`. TypeScript works out every string the pattern allows and marks any other string before the code runs.
Template types look like template literals
Here is the same idea as plain JavaScript, with a value instead of a type:
const size = "small";
const className = `btn-${size}`;
console.log(className);Line 1 stores the text "small". Line 2 puts the value of size into the placeholder while the code runs. Line 3 prints btn-small: one string. A template literal type uses the same backticks, but its placeholder holds a type, and the result is a type: every string that fits.
The table below compares the two. Read each row as a question, then the answer for each.
| Question | A template literal (a value) | A template literal type (a type) |
|---|---|---|
| Where do you write it? | In code that runs, like const className = .... | In a type, like type ButtonClass = .... |
What goes inside ${ }? | A value, like the variable size. | A type, like Size, string, or number. |
| What do you get? | One string, like "btn-small". | A string type: every string that fits the pattern. |
| When does it happen? | While the code runs. | Before the code runs. The type is removed after the check. |
The placeholder doesn't have to hold a union. It can hold number, which makes a pattern for CSS sizes:
type Pixels = `${number}px`; const width: Pixels = "120px";const gap: Pixels = "1.5px";const height: Pixels = "tall";const margin: Pixels = 120;Line 1 says a Pixels value is text that reads as a number, followed by px. Lines 3 and 4 fit: 120 and 1.5 both read as numbers. TypeScript marks two lines on purpose:
- Line 5,
"tall":Type '"tall"' is not assignable to type'`${number}px`'. The texttalldoesn't read as a number. - Line 6,
120:Type '120' is not assignableto the same type. A template literal type is always a string type, and120without quotes is a number, not text.
A string placeholder takes any text at all. User IDs that must start with user- look like this:
type UserId = `user-${string}`; const asha: UserId = "user-asha";const sam: UserId = "user-42";const guest: UserId = "guest-7";Line 1 says a UserId is user- followed by any text. Lines 3 and 4 fit, whatever comes after the dash. Line 5 starts with guest-, so TypeScript marks it on purpose: the fixed text has to be there exactly.
The table below lists what each kind of placeholder lets in. Read each row as: this type in the placeholder, then which strings fit.
| In the placeholder | Which strings fit | An example |
|---|---|---|
A union of strings, like Size | Only the union's members, one at a time. | "btn-small" fits the pattern btn-${Size}. |
string | Any text at all, even no text. | "user-asha" and even "user-" fit user-${string}. |
number | Text that reads as a number, like 120 or 1.5. | "120px" and "1.5px" fit ${number}px. |
boolean | Only the text true or the text false. | "true" fits ${boolean}, but "yes" doesn't. |
A union fills in every combination
A pattern can have more than one placeholder. A toast is a small message that pops up in a corner of the page, like "Saved". Its position is a side and an alignment:
type Side = "top" | "bottom";type Align = "left" | "right";type ToastPosition = `${Side}-${Align}`;// ^?const toast: ToastPosition = "bottom-right";console.log(toast);Line 1 lists two sides and line 2 two alignments. Line 3 joins a side, a dash, and an alignment. The query on line 4 shows that TypeScript made every combination: "top-left", "top-right", "bottom-left", and "bottom-right". Line 5 picks one of them, and line 6 prints bottom-right.
ToastPosition.Change the sides on line 1, and watch the answer after the query on line 4:
type Side = "top" | "bottom";type Align = "left" | "right";type ToastPosition = `${Side}-${Align}`;// ^?const toast: ToastPosition = "bottom-right";console.log(toast);4 names2 × 2 = 4No errorsTwo sides and two alignments make 4 names. "bottom-right" is one of them, so line 5 has no error.
With Only top, there are 2 names, and line 5's "bottom-right" isn't one of them. With Add center, there are 6: the count is always the number of sides times the number of alignments. Each union you add multiplies again, so the count grows fast. TypeScript refuses a pattern that would make 100,000 names or more:
type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";type PostalCode = `${Digit}${Digit}${Digit}${Digit}${Digit}`;Line 1 lists the ten digits. Line 2 asks for five of them in a row, like a five-digit postal code. That is 10 × 10 × 10 × 10 × 10, which is 100,000 names, so TypeScript marks line 2 on purpose: Expression produces a union type that is too complex to represent. For a part that can be almost anything, put string or number in the placeholder instead of a long union.
Uppercase, Lowercase, and Capitalize
Names in code often need a capital letter where the data has none. React components, for example, take handler props like onClick for the click event. TypeScript has four built-in helpers that change the case of a string type. You write the helper's name and then a string type in angle brackets, the way you pass a type to a generic:
type Loud = Uppercase<"save">;// ^?type Quiet = Lowercase<"SAVE">;// ^?type Title = Capitalize<"save">;// ^?type Plain = Uncapitalize<"Save">;// ^?Line 1 turns "save" into "SAVE", as the query on line 2 shows. Line 3 does the opposite. Line 5 changes only the first letter, so the query on line 6 shows "Save", and line 7 makes the first letter small again.
The table below sums them up. Read each row as: this helper, what it does, and an example.
| Helper | What it does to the text | An example |
|---|---|---|
Uppercase | Makes every letter a capital. | Uppercase<"save"> is "SAVE". |
Lowercase | Makes every letter small. | Lowercase<"SAVE"> is "save". |
Capitalize | Makes only the first letter a capital. | Capitalize<"save"> is "Save". |
Uncapitalize | Makes only the first letter small. | Uncapitalize<"Save"> is "save". |
The helpers work inside a pattern, too. Here line 2 builds handler names from a button's events. Pick a different helper and watch whether line 4's "onClick" still fits:
type ButtonEvent = "click" | "focus";type HandlerName = `on${Capitalize<ButtonEvent>}`;// ^?const handler: HandlerName = "onClick";console.log(handler);"onClick" | "onFocus""onClick"No errorsCapitalize turns the first letter into a capital: "click" becomes "Click". The names are "onClick" and "onFocus", so line 4 is fine.
Only Capitalize makes "onClick". Uppercase makes "onCLICK", and Lowercase or no helper leaves "onclick". Strings are case-sensitive, so each of those is a different name, and line 4 gets an error.
The helpers change types. They don't know about the string methods you call while the code runs:
const loud = "save".toUpperCase();// ^?console.log(loud);Line 1 calls .toUpperCase() on "save", and line 3 prints SAVE. But the query on line 2 shows const loud: string, not "SAVE". TypeScript knows the method gives back some string. It doesn't work out which one.
Typed event names
A profile page lets a user change their name, email, and age, and other parts of the page listen for those changes. The events are called nameChanged, emailChanged, and ageChanged. You could list them by hand, but then a new property needs a new event name too. Build them from the object type instead:
type Profile = { name: string; email: string; age: number };type ProfileEvent = `${keyof Profile}Changed`;// ^? function on(event: ProfileEvent, handler: () => void) { console.log(`Listening for ${event}`);} on("nameChanged", () => {});on("name", () => {});Line 1 describes a profile. On line 2, keyof Profile means "the names of Profile's properties", the union "name" | "email" | "age". The pattern adds Changed to each, and the query on line 3 shows the three event names. Lines 5 to 7 make an on function that takes one of them. This one only prints what it listens for; a real one would save the handler and call it later.
Line 9 passes "nameChanged", which fits. Line 10 passes the property name "name", a common slip, and TypeScript marks it on purpose: Argument of type '"name"' is not assignable to parameter of type '"nameChanged" | "emailChanged" | "ageChanged"'. An argument is a value you pass to a function, and a parameter is the name that receives it. The code still prints both lines when it runs.
The handler doesn't know what changed yet: a new age is a number, but a new name is text. TypeScript can work that out from the event name. This uses a type parameter K with a keyof constraint, as in the Generic constraints lesson:
type Profile = { name: string; email: string; age: number }; function on<K extends keyof Profile>( event: `${K}Changed`, handler: (value: Profile[K]) => void,) { console.log(`Listening for ${event}`);} on("ageChanged", (value) => console.log(value));// ^?Line 3 says K must be one of Profile's property names. Line 4 says the event is K followed by Changed. Line 5 says the handler gets a value of type Profile[K], the type of the property that K names. Line 10 passes "ageChanged". TypeScript asks which K makes `${K}Changed` equal "ageChanged", and finds "age". So the query on line 11 shows (parameter) value: number. When it runs, line 7 prints Listening for ageChanged.
"ageChanged" fits the pattern when K is "age". Then Profile["age"] gives the type of value.Pass a different event name on line 10 and watch the type of value:
type Profile = { name: string; email: string; age: number }; function on<K extends keyof Profile>( event: `${K}Changed`, handler: (value: Profile[K]) => void,) { console.log(`Listening for ${event}`);} on("ageChanged", (value) => console.log(value));// ^?number"ageChanged"No errorsTypeScript matches "ageChanged" against the pattern on line 4, so K is "age". Profile[K] is the type of age, number, and that's the type of value.
"nameChanged" and "emailChanged" give a string. "age" and "ageChange" don't fit the pattern, so line 10 gets an error, and TypeScript can only say value is string | number.
Analytics events and handler names
Shops record what shoppers do, like opening the cart, in an analytics tool. The names have to be exactly right, or the reports miss them. A pattern keeps them right:
type Area = "cart" | "checkout";type Action = "open" | "close";type AnalyticsEvent = `${Area}:${Action}`; function track(event: AnalyticsEvent) { console.log(`Tracked ${event}`);} track("cart:open");track("checkout:close");track("cart:opne");Lines 1 and 2 list the areas and the actions. Line 3 joins them with a colon, which makes four names, from "cart:open" to "checkout:close". Lines 5 to 7 make a track function that takes only those names. Lines 9 and 10 pass real names. Line 11 has two letters swapped, "cart:opne", and TypeScript marks it on purpose. Without the type, this typo would print Tracked cart:opne and send a name nobody looks for.
Sometimes your code builds the name while it runs, such as a handler name from an event. As you saw, string methods give back only string. When you're sure what the code makes, you can tell TypeScript with as:
type FormEvent = "submit" | "reset"; function handlerName<E extends FormEvent>(event: E) { const name = `on${event[0].toUpperCase()}${event.slice(1)}`; return name as `on${Capitalize<E>}`;} const submitHandler = handlerName("submit");// ^?console.log(submitHandler);Line 3 makes a function for one form event, E. Line 4 builds the name: on, the first letter in capitals, then the rest of the event. Line 5 says "this string is `on${Capitalize<E>}`". TypeScript trusts you here, so line 4 must really do what the type says. Line 8 calls it with "submit", the query on line 9 shows the exact type "onSubmit", and line 10 prints onSubmit.
const button: `btn-${"small" | "large"}` = "btn-large";const button: `btn-${"small" | "large"}` = "btn-medium";const width: `${number}px` = "40px";const width: `${number}px` = "40";const handler: `on${Capitalize<"click">}` = "onClick";const handler: `on${Capitalize<"click">}` = "onclick";const id: `user-${string}` = "user-";const flag: `${boolean}` = "yes";
Sort each line by what TypeScript says about it. Each line is checked on its own.
Common mix-ups
A template literal type checks strings that TypeScript can see in your code. Text that arrives while the page runs is different. Here a shopper types a width into a box:
type Pixels = `${number}px`; const widthInput = document.createElement("input");widthInput.value = "120px"; const width: Pixels = widthInput.value;console.log(width);Line 3 makes an <input> text box, and line 4 puts 120px in it, as if the shopper typed it. Line 6 stores the box's value as Pixels, and TypeScript marks it on purpose: Type 'string' is not assignable to type '`${number}px`'. A text box can hold any text, so TypeScript only knows the value is a string. It can't know the shopper typed something that fits. The fix is to check the text while the code runs:
type Pixels = `${number}px`; function isPixels(text: string): text is Pixels { return /^\d+(\.\d+)?px$/.test(text);} const widthInput = document.createElement("input");widthInput.value = "120px"; const text = widthInput.value;if (isPixels(text)) { const width: Pixels = text; console.log(width);}Lines 3 to 5 make isPixels, a type guard. Line 4 tests the text with a regular expression: digits, maybe a decimal part, then px. Its return type, text is Pixels, tells TypeScript that a true answer means the text fits. So inside the if on line 11, line 12 can store it as Pixels, and line 13 prints 120px.
The table below lists where text comes from. Read each row as: text from here, then what TypeScript knows about it.
| Where the text comes from | What TypeScript knows about it |
|---|---|
A string written in your code, like "120px" | Its exact text. TypeScript checks it against Pixels before the code runs. |
| Text from an input, a server, or the address bar | Only that it's a string. It can't go where Pixels is needed until your code checks it. |
Text your code builds with .toUpperCase() or + | Usually only string. Check it, or tell TypeScript with as when you're sure. |
- "A template literal type makes a string." No. It describes which strings are allowed. Your code still writes or builds every string, and the type is gone when the code runs.
- "TypeScript checks what users type against the pattern." No. Text from an input or a server is only a
stringto TypeScript. Check it with your own code, likeisPixels. - "
Uppercasechanges my values." No. It changes types. To change a value, call.toUpperCase(), whose type is juststring. - "Two unions in a pattern add up." No. They multiply: 2 sides and 3 alignments make 6 names, not 5.
- "
`${number}px`takes the number 120." No. It takes text, like"120px". Every template literal type is a string type. - "Case doesn't matter in a pattern." No.
"onclick"and"onClick"are different strings, which is why the case helpers exist.
Practice exercises
Each exercise is TypeScript. To change the starter code, press Edit & run: the errors update as you type, and a // ^? line under a type shows TypeScript's answer. Try a guess first, then use the hints.
What strings does GreetingKey contain? Guess first. Then press Edit & run and add a // ^? line under line 2 to check.
type Lang = "en" | "fr";type GreetingKey = `greeting-${Lang}`;type Lang = "en" | "fr";type GreetingKey = `greeting-${Lang}`;// ^?Lang has two members, so the pattern makes two strings: "greeting-en" and "greeting-fr". The query on line 3 shows type GreetingKey = "greeting-en" | "greeting-fr".
Two theme classes, one of them wrong. Which line will TypeScript mark?
type Theme = "light" | "dark";
type ThemeClass = `theme-${Theme}`;
const page: ThemeClass = "theme-dark";
const card: ThemeClass = "dark-theme";type Theme = "light" | "dark";type ThemeClass = `theme-${Theme}`; const page: ThemeClass = "theme-dark";const card: ThemeClass = "theme-light";ThemeClass is "theme-light" | "theme-dark". Line 5's "dark-theme" has the parts the wrong way round, so TypeScript marks it. The fix uses a real name, like "theme-light".
Size is just string, so the broken margin on line 5 gets no error. Change line 1 so that sizes in px and rem are allowed and anything else is an error. Which line gets the error then?
type Size = string; const gap: Size = "8px";const padding: Size = "1.5rem";const margin: Size = "large";type Size = `${number}px` | `${number}rem`; const gap: Size = "8px";const padding: Size = "1.5rem";const margin: Size = "large";The two patterns allow any number followed by px or by rem. "8px" and "1.5rem" fit. "large" doesn't read as a number, so line 5 now gets an error, which is the point: TypeScript catches it before a page gets a broken margin.
A modal window has open and close events, and its props are named like onOpen. TypeScript marks line 4. Change only line 2 so that "onOpen" fits.
type ModalEvent = "open" | "close";type ModalHandler = `on${ModalEvent}`; const handler: ModalHandler = "onOpen";console.log(handler);type ModalEvent = "open" | "close";type ModalHandler = `on${Capitalize<ModalEvent>}`; const handler: ModalHandler = "onOpen";console.log(handler);Capitalize<ModalEvent> is "Open" | "Close", so the pattern makes "onOpen" and "onClose". Line 4 fits, and the code prints onOpen.
A cart page listens for an update to its total. TypeScript marks line 8. What event name should it pass?
type Cart = { items: number; total: number };type CartEvent = `${keyof Cart}Updated`; function on(event: CartEvent, handler: () => void) { console.log(`Listening for ${event}`);} on("totalUpdate", () => {});type Cart = { items: number; total: number };type CartEvent = `${keyof Cart}Updated`; function on(event: CartEvent, handler: () => void) { console.log(`Listening for ${event}`);} on("totalUpdated", () => {});CartEvent is "itemsUpdated" | "totalUpdated". Line 8 was missing the final d. With "totalUpdated" the error is gone, and the code prints Listening for totalUpdated.
The handler's value is unknown, a type that lets you do nothing with it until you check it, so line 8 gets 'value' is of type 'unknown'. Change on so that value gets the right type from the event name. What type does it get for "couponChanged"?
type Cart = { items: number; coupon: string }; function on(event: `${keyof Cart}Changed`, handler: (value: unknown) => void) { console.log(`Listening for ${event}`);} on("couponChanged", (value) => { console.log(value.toUpperCase());});type Cart = { items: number; coupon: string }; function on<K extends keyof Cart>(event: `${K}Changed`, handler: (value: Cart[K]) => void) { console.log(`Listening for ${event}`);} on("couponChanged", (value) => {// ^? console.log(value.toUpperCase());});With K in the pattern, "couponChanged" makes K equal "coupon". The handler's value is Cart["coupon"], a string, so .toUpperCase() is allowed. The query on line 8 shows (parameter) value: string.
Check your understanding
For each question, write out the strings the pattern makes before you choose. The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What does
SizeLabelcontain?Read the code, then predicttype Size = "S" | "M"; type SizeLabel = `size-${Size}`;Choose an answer to see the explanation.
Question 2 of 7How many names does this
ToastPositionhold?Read the code, then predicttype Side = "top" | "bottom"; type Align = "left" | "center" | "right"; type ToastPosition = `${Side}-${Align}`;Choose an answer to see the explanation.
Question 3 of 7What is the
Handlertype?Read the code, then predicttype Handler = `on${Capitalize<"click">}`;Choose an answer to see the explanation.
Question 4 of 7What type does TypeScript give
loud, and what does line 2 print?Read the code, then predictconst loud = "tea".toUpperCase(); console.log(loud);Choose an answer to see the explanation.
Question 5 of 7Which line does TypeScript flag in this profile event code?
Read the code, then predicttype Profile = { name: string; age: number }; type ProfileEvent = `${keyof Profile}Changed`; function on(event: ProfileEvent) { console.log(event); } on("ageChanged"); on("age");Choose an answer to see the explanation.
Question 6 of 7Does TypeScript accept the width on line 2?
Read the code, then predicttype Pixels = `${number}px`; const width: Pixels = "2.5px";Choose an answer to see the explanation.
Question 7 of 7What is left of
type ButtonClasswhen the page runs?Choose an answer to see the explanation.
Key takeaways
- A template literal type is written like a template literal, with types in the placeholders, like
`btn-${Size}`. - A union in a placeholder makes one string per member. Two unions make every combination, so the counts multiply.
stringandnumberin a placeholder make open patterns, like`${number}px`.booleanallows only the text"true"or"false".Uppercase,Lowercase,Capitalize, andUncapitalizechange the case of string types, not values.`${keyof T}Changed`builds event names from an object's keys, and a genericKlets the handler's value follow the name.- Text that arrives while the page runs is only a
string. Check it before you use it as a pattern type.
Remember the one-liner.
A template literal type is a pattern for strings, and TypeScript checks every string in your code against it.
Coming next: Partial, Pick, Omit & friends. It covers TypeScript's built-in helpers that make new object types from old ones, like every property optional for a form update.