How to Use Template Literal Types in TypeScript

Updated: January 8, 2024 By: Guest Contributor Post a comment

Introduction

Since TypeScript 4.1, developers have been graced with an exciting new feature known as template literal types. This amalgamation of template literals and type logic has opened up a vibrant avenue for more expressive and powerful type constructs.

Template literal types in TypeScript provide the ability to create complex type relationships by interpolating strings within types. They are as flexible as JavaScript template literals, and yet they leverage TypeScript’s static type system to enforce type correctness at compile time.

Basic Usage

To start, let’s grasp template literal types with a rudimentary example. Suppose we have a function that concatenates a customer’s title with their name.

type Title = 'Mr' | 'Ms' | 'Dr';
function createFullName(title: Title, name: string): `${Title} ${string}` {
  return `${title} ${name}`;
}
const fullName = createFullName('Dr', 'Alice'); // 'Dr Alice'

Toying with this notion further, imagine we require a type for specific button classes based on their purpose in a UI library.

type ButtonPurpose = 'submit' | 'reset' | 'cancel';

type ButtonClass = `${ButtonPurpose}-button`;

const submitButtonClass: ButtonClass = 'submit-button'; // Correct
const closeButtonClass: ButtonClass = 'close-button'; // Error: Type '"close-button"' is not assignable to type 'ButtonClass'

Now let us elevate our discussion with advanced examples.

Advanced Patterns

Typescript’s template literal types become more potent when allied with mapped types. We can define types based on the transformations of existing types or interfaces.

interface Routes {
  home: void;
  about: void;
  contact: number;
}

type RouteNames = keyof Routes;

type Path = `/${RouteNames}`;

const homePath: Path = '/home'; // Correct
const loginPath: Path = '/login'; // Error: Type '"/login"' is not assignable to type 'Path'

The horizon of template literal types stretches far, empowering us to declare types for various API endpoint concatenations and beyond.

type APIEndpoint = `/api/${T}`;

type UserAPI = APIEndpoint<'users'> | APIEndpoint<'items'>;

const usersEndpoint: UserAPI = '/api/users'; // Correct
const productsEndpoint: UserAPI = '/api/products'; // Error: Type '"/api/products"' is not assignable to type 'UserAPI'

Another striking practice is to concatenate literal types conditionally by elevating union types to the realm of template literals.

type LoadingState = 'loading';
type FetchedState = 'fetched';
type ErrorState = 'error';

type ResourceState = `${LoadingState | FetchedState | ErrorState}`;

type ResourceStateMessage = `${ResourceState | 'idle'}-message`;

const loadingMessage: ResourceStateMessage = 'loading-message';   // Correct
const completedMessage: ResourceStateMessage = 'completed-message'; // Error

Introducing Inference with Template Literals

The union with inference using template literals caters to more dynamic scenarios, wherein we wish to extract parameters from strings and leverage them within type logic.

type ExtractIdFromPath = Path extends `/items/${infer Id}` ? Id : never;

type ItemId = ExtractIdFromPath<'/items/42'>; // '42'
const itemId: ItemId = '42'; // Correct
const invalidItemId: ItemId = 'abc'; // Error

Our journey into the rabbit hole unveils the prowess of these types in more sophisticated realms such as enforcing function parameter relationships.

type Event = {
  type: 'click' | 'mouseover';
} & {
  timestamp: number;
};

function handleEvent(
  eventType: `${Type}-event`,
  handler: (event: Event & { type: Type }) => void
) {
  // Implementation
}

handleEvent('click-event', (event) => {}); // Correct
handleEvent('keypress-event', (event) => {}); // Error: Argument of type '"keypress-event"' is not assignable to parameter of type '"click-event" | "mouseover-event"'

Conclusion

In summary, TypeScript’s template literal types imbue the static type system with a dynamic spirit, offering the meticulous developer a toolset for constructing rigorous type relations reflective of the application’s domain. It behooves any keen TypeScript artisan to harness this feature and etch their codebase with the stamp of type precision and clarity.