Template Literal Types
Template literal types build on string literal types, and have the ability to expand into many strings via unions.
They have the same syntax as template literal strings in JavaScript, but are used in type positions. When used with concrete literal types, a template literal produces a new string literal type by concatenating the contents.
When a union is used in the interpolated position, the type is the set of every possible string literal that could be represented by each union member:
For each interpolated position in the template literal, the unions are cross multiplied:
We generally recommend that people use ahead-of-time generation for large string unions, but this is useful in smaller cases.
The power in template literals comes when defining a new string based on information inside a type.
Consider the case where a function ( makeWatchedObject ) adds a new function called on() to a passed object. In JavaScript, its call might look like: makeWatchedObject(baseObject) . We can imagine the base object as looking like:
The on function that will be added to the base object expects two arguments, an eventName (a string ) and a callback (a function ).
The eventName should be of the form attributeInThePassedObject + «Changed» ; thus, firstNameChanged as derived from the attribute firstName in the base object.
The callback function, when called:
- Should be passed a value of the type associated with the name attributeInThePassedObject ; thus, since firstName is typed as string , the callback for the firstNameChanged event expects a string to be passed to it at call time. Similarly events associated with age should expect to be called with a number argument
- Should have void return type (for simplicity of demonstration)
The naive function signature of on() might thus be: on(eventName: string, callback: (newValue: any) => void) . However, in the preceding description, we identified important type constraints that we’d like to document in our code. Template Literal types let us bring these constraints into our code.
Notice that on listens on the event «firstNameChanged» , not just «firstName» . Our naive specification of on() could be made more robust if we were to ensure that the set of eligible event names was constrained by the union of attribute names in the watched object with “Changed” added at the end. While we are comfortable with doing such a calculation in JavaScript i.e. Object.keys(passedObject).map(x => `$Changed`) , template literals inside the type system provide a similar approach to string manipulation:
With this, we can build something that errors when given the wrong property:
Inference with Template Literals
Notice that we did not benefit from all the information provided in the original passed object. Given change of a firstName (i.e. a firstNameChanged event), we should expect that the callback will receive an argument of type string . Similarly, the callback for a change to age should receive a number argument. We’re naively using any to type the callback ’s argument. Again, template literal types make it possible to ensure an attribute’s data type will be the same type as that attribute’s callback’s first argument.
The key insight that makes this possible is this: we can use a function with a generic such that:
- The literal used in the first argument is captured as a literal type
- That literal type can be validated as being in the union of valid attributes in the generic
- The type of the validated attribute can be looked up in the generic’s structure using Indexed Access
- This typing information can then be applied to ensure the argument to the callback function is of the same type
Here we made on into a generic method.
When a user calls with the string «firstNameChanged» , TypeScript will try to infer the right type for Key . To do that, it will match Key against the content before «Changed» and infer the string «firstName» . Once TypeScript figures that out, the on method can fetch the type of firstName on the original object, which is string in this case. Similarly, when called with «ageChanged» , TypeScript finds the type for the property age which is number .
Inference can be combined in different ways, often to deconstruct strings, and reconstruct them in different ways.
Intrinsic String Manipulation Types
To help with string manipulation, TypeScript includes a set of types which can be used in string manipulation. These types come built-in to the compiler for performance and can’t be found in the .d.ts files included with TypeScript.
Converts each character in the string to the uppercase version.
Template literal types typescript
In this article, we will take a closer look at template literal types and how you can take advantage of them in your day-to-day activities as a developer.
So, what are template literal types?
Literal Types
In order to understand what template literal types are, we first need to have a brief look at literal types. Literal types allow us to define types that are more specific, instead of something that is generalized like string or number.
Let’s say you have a switch; it can have the value of either on or off. One way of defining the types of this, is to use literal types, giving it the type of either On or Off :
In the case above, the value of any variable of type Switch can only be On or Off :
const x: Switch = "On" const y: Switch = "Off"
If you tried to assign any other values other than On or Off , typescript will throw an error:
Template Literal Types
Template Literal Types build on this, allowing you to build new types using a template and can expand to many different string using Unions. This works just like template literal/strings, but instead of concatenating to form strings, it concatenates to form types.
const variable = "string"; type tVariable = "string"; // this results to a variable const val = `This is a concatenated $` // while this results to type type X = `This is a concatenated $`
As you can see, they are similar in syntax apart from what they are defined as, the first being a variable and the second being a type. The type of the first definition will be string, while the second one will be of type This is a concatenated string and a variable of that type can only be assigned to that string.
NB: If you tried to use variable instead of a type when defining Template Literal Type, it will throw the following error: ‘variable’ refers to a value, but is being used as a type here. Did you mean ‘typeof variable’?
If we take our example above of type Switch, we may want to have a function that returns the status of the switch, i.e. Switch is On or Switch is Off , and have it strongly typed, in that it can only return only those strings. With Template Literal Types, we can define this as follows:
type Switch = "On" | "Off" const x: Switch = "On" const y: Switch = "Off" type SwitchStatus = `Switch is $`;
And this in return gives us the types: Switch is On and Switch is Off :
Using To Build Types for Grid Items Coordinates
Let’s say we are working with a grid system, and wanted to perform a task on various boxes in our grid, like placing something on a specific box given its coordinates. It would be nice if we could strongly type it and ensure we don’t specify values outside the grid.
For instance, if we had a grid whose length was 3 smaller boxes on either side of the box. This makes it that we have 9 smaller box fitting on our big box. We can use literal types to create a type for each of our boxes, with the type being its position in the grid. So, the first gets L1-H1 and the last gets L3-H3 types, as shown below.
type SquareBoxes = "L1-H1" | "L1-H2" | "L1-H3" | "L2-H1" | "L2-H2" | "L2-H3" | "L3-H1" | "L3-H2" | "L3-H3";
Those are a lot of types to create by hand even for a small grid of 9 boxes. But, with template literals types, we could define just the type of the length of one side and use template string literals to expand the rest of the types:
type length = "1" | "2" | "3"; type SmallerBoxes = `L$-H$`
And this would yield the same result as before:
This makes our work easier and it is more versatile, because if the smaller boxes ever increased or decreased, you only need to adjust the size of the length.
// 16 boxes type length = "1" | "2" | "3" | "4"; // 25 boxes type length = "1" | "2" | "3" | "4" | "5"; // 4 boxes type length = "1" | "2";
Combining With Generics
We can combine template literal types with generics to some amazing effect. Let’s take with a Type of Person , which has two properties — name and age .
We want to add two methods to be called to update the values of name or age i.e. nameChanged or ageChanged . We can create a new type, that will take type Person as a generic, and for each property of type Person , we will add new properties with Changed appended the original properties of type Person i.e. nameChanged and ageChanged . We will used template literal types to create a new property by appending Changed to the property name.
type WithPersonChangedEvents = < [Property in keyof Type as `$Changed`]: (newValue: Type[Property]) => void; > & Type;
NB: The above example uses some advanced typescript technique for manipulating types on top of Template Literal Types which you can learn more here.
Now, we can use both of our Types (Person and WithPersonChangedEvent) above:
const person: WithPersonChangedEvents = < name: "Name", age: 20, nameChanged: (newName) =>console.log(newName), ageChanged: (newAge) => console.log(newAge), >; person.ageChanged(21); // Logs: 21 person.nameChanged("new Name"); // Logs: "new Name"
And as you can see, our object — person has 4 properties, with 2 being the added methods.
Conclusion
We have learned about Template Literal Types in Typescript and how they build on top Literal types to provide you even more flexibility when defining types. We have also looked at different use cases like in a grid system type definition for different boxes coordinates and combining them with generics to define extra properties for an object.
Resources
- Creating Types from Types — Link.
- Template Literal Types Documentation — Link.
- Template literals (Template strings) — Link.
- Types and Mocking — Typescript — Link.
- Transforming Types in TypeScript with Utility Types — Link.