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. When used with concrete literal types, a template literal produces a new string literal type by concatenating the contents.

  1. type World = "world";
  2. type Greeting = `hello ${World}`;
    // ^ = type Greeting = "hello world"

When a union is used in the interpolated position, the type is the set of every possible string literal that could be represented by each union member:

  1. type EmailLocaleIDs = "welcome_email" | "email_heading";
    type FooterLocaleIDs = "footer_title" | "footer_sendoff";
  2. type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
    // ^ = type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"

For each interpolated position in the template literal, the unions are cross multiplied:

  1. type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
    type Lang = "en" | "ja" | "pt";
  2. type LocaleMessageIDs = `${Lang}_${AllLocaleIDs}`;
    // ^ = type LocaleMessageIDs = "en_welcome_email_id" | "en_email_heading_id" | "en_footer_title_id" | "en_footer_sendoff_id" | "ja_welcome_email_id" | "ja_email_heading_id" | "ja_footer_title_id" | "ja_footer_sendoff_id" | "pt_welcome_email_id" | "pt_email_heading_id" | "pt_footer_title_id" | "pt_footer_sendoff_id"

We generally recommend that people use ahead-of-time generation for large string unions, but this is useful in smaller cases.

String Unions in Types

The power in template literals comes when defining a new string based off an existing string inside a type.

For example, a common pattern in JavaScript is to extend an object based on the fields that it currently has. We’ll provide a type definition for a function which adds support for an on function which lets you know when a value has changed:

  1. const person = makeWatchedObject({
    firstName: "Saoirse",
    lastName: "Ronan",
    age: 26,
    });
  2. person.on("firstNameChanged", (newValue) => {
    console.log(`firstName was changed to ${newValue}!`);
    });

Notice that on listens on the event "firstNameChanged", not just "firstName", template literals provide a way to handle this sort of string manipulation inside the type system:

  1. type PropEventSource<Type> = {
    on(eventName: `${string & keyof Type}Changed`, callback: (newValue: any) => void): void;
    };
  2. /// Create a "watched object" with an 'on' method
    /// so that you can watch for changes to properties.
    declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;

With this, we can build something that errors when given the wrong property:

  1. const person = makeWatchedObject({
    firstName: "Saoirse",
    lastName: "Ronan",
    age: 26
    });
  2. person.on("firstNameChanged", () => {});
  3. // It's typo-resistent
    person.on("firstName", () => {});
    Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.2345Argument of type '"firstName"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.
  4. person.on("frstNameChanged", () => {});
    Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.2345Argument of type '"frstNameChanged"' is not assignable to parameter of type '"firstNameChanged" | "lastNameChanged" | "ageChanged"'.

Inference with Template Literals

Note how the last examples did not re-use the type of the original value. The callback used an any. Template literal types can infer from substitution positions.

We can make our last example generic to infer from parts of the eventName string to figure out the associated property.

  1. type PropEventSource<Type> = {
    on<Key extends string & keyof Type>
    (eventName: `${Key}Changed`, callback: (newValue: Type[Key]) => void ): void;
    };
  2. declare function makeWatchedObject<Type>(obj: Type): Type & PropEventSource<Type>;
  3. const person = makeWatchedObject({
    firstName: "Saoirse",
    lastName: "Ronan",
    age: 26
    });
  4. person.on("firstNameChanged", newName => {
    // ^ = (parameter) newName: string
  5. console.log(`new name is ${newName.toUpperCase()}`);
    });
  6. person.on("ageChanged", newAge => {
    // ^ = (parameter) newAge: number
  7. if (newAge < 0) {
    console.warn("warning! negative age");
    }
    })

Here we made on into a generic method.

When a user calls with the string "firstNameChanged', TypeScript will try to infer the right type for K. To do that, it will match K against the content prior to "Changed" and infer the string "firstName". Once TypeScript figures that out, the on method can fetch the type of firstName on the original object, which is string in this case. Similarly, when called with "ageChanged", TypeScript finds the type for the property age which is number.

Inference can be combined in different ways, often to deconstruct strings, and reconstruct them in different ways.

Intrinsic String Manipulation Types

To help with string manipulation, TypeScript includes a set of types which can be used in string manipulation. These types come built-in to the compiler for performance and can’t be found in the .d.ts files included with TypeScript.

Uppercase<StringType>

Converts each character in the string to the uppercase version.

Example
  1. type Greeting = "Hello, world"
    type ShoutyGreeting = Uppercase<Greeting>
    // ^ = type ShoutyGreeting = "HELLO, WORLD"
  2. type ASCIICacheKey<Str extends string> = `ID-${Uppercase<Str>}`
    type MainID = ASCIICacheKey<"my_app">
    // ^ = type MainID = "ID-MY_APP"

Lowercase<StringType>

Converts each character in the string to the lowercase equivalent.

Example
  1. type Greeting = "Hello, world"
    type QuietGreeting = Lowercase<Greeting>
    // ^ = type QuietGreeting = "hello, world"
  2. type ASCIICacheKey<Str extends string> = `id-${Lowercase<Str>}`
    type MainID = ASCIICacheKey<"MY_APP">
    // ^ = type MainID = "id-my_app"

Capitalize<StringType>

Converts the first character in the string to an uppercase equivalent.

Example
  1. type LowercaseGreeting = "hello, world";
    type Greeting = Capitalize<LowercaseGreeting>;
    // ^ = type Greeting = "Hello, world"

Uncapitalize<StringType>

Converts the first character in the string to a lowercase equivalent.

Example
  1. type UppercaseGreeting = "HELLO WORLD";
    type UncomfortableGreeting = Uncapitalize<UppercaseGreeting>;
    // ^ = type UncomfortableGreeting = "hELLO WORLD"

Technical details on the intrinsic string manipulation types

The code, as of TypeScript 4.1, for these intrinsic functions uses the JavaScript string runtime functions directly for manipulation and are not locale aware.

`

  1. function applyStringMapping(symbol: Symbol, str: string) {
  2. switch (intrinsicTypeKinds.get(symbol.escapedName as string)) {
  3. case IntrinsicTypeKind.Uppercase: return str.toUpperCase();
  4. case IntrinsicTypeKind.Lowercase: return str.toLowerCase();
  5. case IntrinsicTypeKind.Capitalize: return str.charAt(0).toUpperCase() + str.slice(1);
  6. case IntrinsicTypeKind.Uncapitalize: return str.charAt(0).toLowerCase() + str.slice(1);
  7. }
  8. return str;
  9. }

`