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
:
type Switch = "On" | "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 ${variable}`
// while this results to type
type X = `This is a concatenated ${tVariable}`
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 ${Switch}`;
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${length}-H${length}`
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
.
type Person = {
name: string;
age: number;
}
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<Type> = {
[Property in keyof Type as `${string & Property}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<Person> = {
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.