Sling Academy
Home/TypeScript/How to Use Template Literal Types in TypeScript

How to Use Template Literal Types in TypeScript

Last updated: January 08, 2024

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.

Next Article: Iterators and Generators in TypeScript: A Complete Guide

Previous Article: How to Use Mapped Types in TypeScript

Series: The First Steps to TypeScript

TypeScript

You May Also Like

  • TypeScript: setInterval() and clearInterval() methods (3 examples)
  • TypeScript sessionStorage: CRUD example
  • Using setTimeout() method with TypeScript (practical examples)
  • Working with window.navigator object in TypeScript
  • TypeScript: Scrolling to a specific location
  • How to resize the current window in TypeScript
  • TypeScript: Checking if an element is a descendant of another element
  • TypeScript: Get the first/last child node of an element
  • TypeScript window.getComputerStyle() method (with examples)
  • Using element.classList.toggle() method in TypeScript (with examples)
  • TypeScript element.classList.remove() method (with examples)
  • TypeScript: Adding Multiple Classes to An Element
  • element.insertAdjacentHTML() method in TypeScript
  • TypeScript – element.innerHTML and element.textContent
  • Using element.removeAttribute() method in TypeScript
  • Working with Document.createElement() in TypeScript
  • Using getElementById() method in TypeScript
  • Using Window prompt() method with TypeScript
  • TypeScript – window.performance.measure() method (with examples)