2 min read

TypeScript 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.

1. Basic Syntax

When used with concrete string types, a template literal produces a new string literal type by concatenating the contents.

type World = "world";
type Greeting = `hello ${World}`;
// type Greeting = "hello world"

2. Union Types

The real power comes when you use unions inside the interpolation. TypeScript distributes the union over the template literal, creating a cross-product of all possibilities.

type Color = "red" | "blue";
type Quantity = "one" | "two";

type Item = `${Color}-${Quantity}`;
// type Item = "red-one" | "red-two" | "blue-one" | "blue-two"

3. Intrinsic String Manipulation Types

TypeScript provides special types to help with string manipulation within types.

  • Uppercase<StringType>
  • Lowercase<StringType>
  • Capitalize<StringType>
  • Uncapitalize<StringType>
type Greeting = "Hello, world";
type ShoutyGreeting = Uppercase<Greeting>;
// type ShoutyGreeting = "HELLO, WORLD"

type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`;
type MainID = ASCIICacheKey<"my_app">;
// type MainID = "ID-MY_APP"

4. Practical Example: Event Listeners

You can use template literals to type dynamic event names.

type PropEventSource<Type> = {
    on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
};

declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;

const person = makeWatchedObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});

// TypeScript expects "firstNameChanged" | "lastNameChanged" | "ageChanged"
person.on("firstNameChanged", (newValue) => {
    console.log(`firstName was changed to ${newValue}!`);
});

// person.on("firstName", () => {}); // Error

programming/javascript/typescript/typescript