Among the vast number of improvements that ES2015 brought to the JavaScript language was a powerful feature called “template literals.” Naturally, TypeScript, as a superset of JavaScript, has supported this useful construct since its beginning. However, TypeScript 4.1 introduced a novel application of the template literal concept to increase the power and usefulness of its type-system: it introduced something called a template literal type. In this article, we’ll talk about this new feature and how you can use it to increase the robustness of one of the most challenging things that a type system can face: strings.

Reviewing Template Literals

Before we jump into the new stuff, let’s take a second to review what a template literal is in JavaScript/TypeScript. Before ES2015, creating data-driven strings required extensive use of string concatenation (i.e., adding string-like values together to make a larger string). This often looked something like this:

type User = {firstName: string, lastName: string};
 
const user = {
    firstName: 'Arthur',
    lastName: 'Dent'
};
 
// use plus sign for string concatenation
let fullName = user.lastName + ', ' + user.firstName;
 
// use concat method
fullName = user.firstName.concat(', ', user.lastName);

While both of these techniques worked, they contain a lot of noise and make it difficult to understand the intent of the code. Template literals remove this clutter by allowing data to be injected directly into the string using placeholders, as seen below.

type User = {firstName: string, lastName: string};
 
const user = {
    firstName: 'Arthur',
    lastName: 'Dent'
};
 
let fullName = `${user.firstName}, ${user.lastName}`;

This example is much easier to read and understand. The template literal uses backticks, or grave accents, to delimit the template. Within those delimiters, we can combine string literals with data values that have been enclosed in curly braces and preceded by a dollar sign – ${data value}. Template literals are potent tools in JavaScript and TypeScript with capabilities that go well beyond what is described here, but this is enough of a review to allow us to get to the reason we’re here – template literal types. If you want to learn more about template literals, MDN has a great page to get you started.

Template Literal Types in TypeScript

The Basics

As we saw above, template literals are used to generate strings using a combination of string literals and data. On the other hand, Template literal types are used to compose existing string-like types together to make a new type. For example, let’s say that we have a very simple string literal type:

type Restaurant = 'Milliways';

A template literal type can use that to build a new type using the template literal syntax, replacing the customary data values with type values like this:

type Greeting = `Hello, welcome to ${Restaurant}!`;

In this example, Greeting is a type that can only match one value – the string “Hello, welcome to Milliways!”. Not terribly useful, but we can build on this basic premise to do some pretty neat things.

Composing String Literals

Let’s replace the string literal type in our previous example with two new ones and then use those to create a template literal type:

type Place = 'Milliways' | 'Betelgeuse' | 'Magrathea';
type CriticalItem = 'Towel' | 'Electronic Thumb' | 'Babel Fish';
 
type Message = `As long as you know where your ${CriticalItem} is, you can get to ${Place}`;

TypeScript’s compiler will ensure that variables created with the Message type only use the places and critical items that we defined. This limits the number of valid Messages to nine – one of three places combined with one of three critical items. While this example is trivial, this capability is handy in many places where multiple strings can be combined. Consider the CSS border property.  In its most general form, it accepts a width, style, and color. Based on what we’ve discussed, we can handle two of those – style and color.

type Style = 'none' | 'dotted' | 'dashed' | 'solid'; // truncated for brevity
type Color = `red` | 'green' | 'blue'; // truncated for brevity
 
type BorderStyle = `${Style} ${Color}`;
 
let borderStyle: BorderStyle = 'solid red';

Of course, there are more ways to define the color property in CSS than we have captured here, but hopefully this makes the point. We can compose string literals together to create strongly typed templates that control a string’s value. However, we do have one missing attribute. Currently, our BorderStyle type doesn’t allow us to specify a width. We want to constrain the width to be defined as a certain number of pixels, but we have a problem: there are an infinite number of valid widths. We need a way to tell the template that it should accept a number without defining all of the valid values. Fortunately, template literal types have this covered.

Using String-like types and Generics with Template Literal Types

So far, all of our examples have defined template literal types using string literal types or other template literal types. TypeScript also allows us to simply specify the type of data that will be used. With that in mind, we can finish our example.

type Style = 'none' | 'dotted' | 'dashed' | 'solid'; // truncated for brevity
type Color = `red` | 'green' | 'blue'; // truncated for brevity
 
type BorderStyle = `${number}px ${Style} ${Color}`;
 
let borderStyle: BorderStyle = '3px solid red';

The border width in our example is not constrained to have a specific value. As long as a valid number is provided, everything works. The ability to constrain the type of a placeholder unlocks even more possibilities when combined with generics. 

The TypeScript documentation introduces an excellent example of how template literal types can be combined with type matching and generics to codify design intent, so we’re going to base our example on theirs. A common task in many TypeScript APIs is providing the capability to watch an object’s properties and, when they change, trigger some action. We’ll use a hypothetical function, makeWatchedObject, that accepts an object literal and returns an object with an on method that can be used to register callback functions that are invoked with given property changes.

type PropEventSource = {
    on(eventName: string, callback: (newValue: any) => void): void;
}
 
declare function makeWatchedObject(obj: any): PropEventSource;
 
const product = makeWatchedObject({
    name: 'Babel Fish',
    quantity: 42
});
 
product.on('quantityChanged', (newValue) => {});
 
product.on('quantity', (newValue) => {}); // doesn't follow convention
 
product.on('nmaeChanged', (newValue) => {}); // typo in property name

This example performs the task that we want, but it isn’t very type-safe. The first argument to the on method is intended to be formed by taking an existing property and adding the suffix “Changed,” as seen above. However, nothing in this example ensures that this convention is followed and no typos are introduced. Using template literal types with type constraints, we can fix this issue.

type PropEventSource<Type> = {
    on<Key extends string & keyof Type>
        (eventName: `${Key}Changed`, callback: (newValue: any) => void): void;
}
 
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
 
const product = makeWatchedObject({
    name: 'Babel Fish',
    quantity: 42
});
 
product.on('quantityChanged', (newValue) => {});
 
product.on('quantity', (newValue) => {}); // doesn't follow convention
 
product.on('nmaeChanged', (newValue) => {}); // typo in property name

In this example, the PropEventSource has been transformed into a generic type. The on method that it contains is also generic and leverages a union type to ensure that the Key is both a string and a key of the Type that is being made watchable. The strongly typed Key is then used in a template literal type to constrain the permissible eventNames to follow the convention that our API expects, removing guesswork and the need for separate documentation. 

We can, in fact, take the improvements one step further. The PropEventSource knows exactly which property we’re trying to watch. We can use that to derive the type of the newValue parameter that is passed into the callback function like so:

type PropEventSource<Type> = {
    on<Key extends string & keyof Type>
        (eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void): void;
}
 
declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
 
const product = makeWatchedObject({
    name: 'Babel Fish',
    quantity: 42
});
 
product.on('quantityChanged', (newValue: number) => {});
 
product.on('nameChanged', (newValue: string) => {});

Leveraging the type information from the generic parameters allows us to strongly type the callback. This, combined with the type-constraints that template literal types enable us to place on the event name, results in a robust, self-documenting block of code that is concise and easy to understand.

Enhancements to Template Literal Types in TypeScript 4.3

While template literal types are very powerful, they had some room for improvement. In TS 4.3, some of those gaps have been closed. The first improvement concerns how TypeScript handles the two different types of placeholders: string-like types and type patterns. When initially released, TypeScript wasn’t able to understand when a template string matched a literal type. Consider this example from the TypeScript documentation:

function bar(s: string): `hello ${string}` {
    // Previously an error, now works!
    return `hello ${s}`;
}

The function, bar, takes a string and returns a template literal with a type constraint. TypeScript can now understand that the returned string matches the required return type. Previously, this would generate the error message:

Type 'string' is not assignable to type '`hello ${string}`'

The other major change is how TypeScript handles assigning variables with separate, but equivalent, template literal types.

declare let s1: `${number}-${number}-${number}`;
declare let s2: `1-2-3`;
declare let s3: `${number}-2-3`;
s1 = s2;
s1 = s3;

The first assignment, s1 = s2, has always worked because TypeScript was able to pattern match s2 into s1. TypeScript 4.3 now can inspect the placeholders between two different template literal types, allowing the second assignment to be used as well.

Summary

TypeScript’s core mission has always been to provide type safety to JavaScript. One of the most challenging areas to bring this safety is when working with strings. The introduction of template literal types to the language provides another tool for developers to draw upon to increase the robustness and clarity of their code.