HomepageExploring TypeScript (TS 5.8 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

38 Template literal types (`${U}`): creating and transforming string literal types

In this chapter, we take a closer look at template literal types in TypeScript: While their syntax is similar to JavaScript’s template literals, they operate at the type level. Their use cases include:

38.1 The structure of this chapter

First, we’ll learn how template literal types work via small toy examples. We’ll write type-level code that looks similar to JavaScript code. These are the topics we’ll cover:

After that, we’ll look at practical examples and neat things that people have done with template literal types.

38.2 Syntax and basic ways of using template literal types

38.2.1 The syntax

The syntax of template literal types is inspired by JavaScript’s template literals. They also let us concatenate values, optionally interspersed with static string fragments:

type Song<Num extends number, Bev extends string> = // (A)
  `${Num} bottles of ${Bev}`
;
type _ = Assert<Equal<
  Song<99, 'juice'>, // (B)
  '99 bottles of juice'
>>;

Explanations:

38.2.2 Concatenation is distributive

If we insert a string literal union type into a template literal type, the latter is applied to each member of the former:

type Modules = 'fs' | 'os' | 'path';
type Prefixed = `node:${Modules}`;
type _ = Assert<Equal<
  Prefixed, 'node:fs' | 'node:os' | 'node:path'
>>;

If we insert more than one string literal union type, then we get the cartesian product (all possible combinations are used):

type Words = `${ 'd' | 'l' }${ 'i' | 'o' }ve`;
type _ = Assert<Equal<
  Words, 'dive' | 'dove' | 'live' | 'love'
>>;

That enables us to concisely specify large unions. We’ll see a practical example later on.

Anders Hejlsberg warns: “Beware that the cross product distribution of union types can quickly escalate into very large and costly types. Also note that union types are limited to less than 100,000 constituents, and the following will cause an error:”

type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
// @ts-expect-error: Expression produces a union type
// that is too complex to represent.
type Zip = `${Digit}${Digit}${Digit}${Digit}${Digit}`;

The above union type has exactly 100,000 elements. This is another example that exceeds the limit:

type Hex =
  | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
  | 'A' | 'B' | 'C' | 'D' | 'E' | 'F'
  ;
// @ts-expect-error: Expression produces a union type
// that is too complex to represent.
type CssColor = `#${Hex}${Hex}${Hex}${Hex}${Hex}${Hex}`;

38.2.3 Extracting substrings via infer

If we use the infer operator inside a template literal, we can extract parts of strings:

type ParseSemver<Str extends string> =
    Str extends `${infer Major}.${infer Minor}.${infer Patch}`
      ? [ Major, Minor, Patch ]
      : never
;
type _ = Assert<Equal<
  ParseSemver<'1.2.3'>, ['1', '2', '3']
>>;

38.2.4 Parsing substrings via infer plus extends

By default an infer inside a template literal extracts a string:

type _ = Assert<Equal<
  '¡Hola!' extends ${infer S}!` ? S : never,
  'Hola'
>>;

If we constrain infer via extends to a type then TypeScript parses values of that type – e.g.:

type _ = [
  Assert<Equal<
    'true' extends `${infer B extends boolean}` ? B : never,
    true
  >>,
  Assert<Equal<
    '256' extends `${infer N extends number}` ? N : never,
    256
  >>,
];

Parsing numbers only works for integers:

type StrToNum<T> =
  T extends `${infer N extends number}` ? N : never
;
type _ = [
  Assert<Equal<
    StrToNum<'123'>, 123
  >>,
  Assert<Equal<
    StrToNum<'-123'>, -123
  >>,
  Assert<Equal<
    StrToNum<'1.0'>, number
  >>,
  Assert<Equal<
    StrToNum<'1e2'>, number
  >>,
  Assert<Equal<
    StrToNum<'abc'>, never
  >>,
];

For an example that uses StrToNum, see “Extracting the indices (numbers) of a tuple” (§37.3.2).

38.2.5 Interpolating primitive types into template literals

So far, we have inserted literal types and union types into template literals, but we can also insert some primitive types. That lets us construct subsets of string:

type Version = `v${number}.${number}`;

// @ts-expect-error: Type '""' is not assignable to
// type '`v${number}.${number}`'.
const version0: Version = '';

const version1: Version = 'v1.0'; // OK

// @ts-expect-error: Type '"v2.zero"' is not assignable to
// type '`v${number}.${number}`'.
const version2: Version = 'v2.zero';

These are the supported types:

const undefinedValue: `${undefined}` = 'undefined';
const nullValue: `${null}` = 'null';
const booleanValue: `${boolean}` = 'true';
const numberValue: `${number}` = '123';
const bigintValue: `${bigint}` = '123';
const stringValue: `${string}` = 'abc';

// @ts-expect-error: Type 'symbol' is not assignable to type
// 'string | number | bigint | boolean | null | undefined'.
const symbolValue: `${symbol}` = 'symbol';

Note that undefined is the type with the single value undefined. null is similar.

38.2.5.1 Example: keeping only strings with numbers

An interpolated type number can be used to “filter” a union of string literal types:

type Properties = '0' | '2' | 'length' | 'toString';
type _ = Assert<Equal<
  Properties & `${number}`, // (A)
  '0' | '2'
>>;

We use the intersection type in line A to extract those elements of Properties that are stringified numbers. Examples that use this trick:

38.2.5.2 Example: excluding strings that start with “a”

The template literal type `a${string}` matches all string literal types that start with the character “a”. Therefore, we can use it to exclude those types:

type _ = Assert<Equal<
  Exclude<'apple' | 'apricot' | 'banana', `a${string}`>,
  'banana'
>>;

38.3 Utility types for string manipulation

TypeScript has four built-in string manipulation types (documentation):

TypeScript (as of version 5.7) uses the following JavaScript methods to make the changes – which are not locale aware (source – see “technical details” at the end):

str.toUpperCase() // Uppercase
str.toLowerCase() // Lowercase
str.charAt(0).toUpperCase() + str.slice(1) // Capitalize
str.charAt(0).toLowerCase() + str.slice(1) // Uncapitalize

38.3.1 Example: isUppercase

We can use Uppercase to define a generic type IsUppercase:

type IsUppercase<Str extends string> = Str extends Uppercase<Str>
  ? true
  : false;

type _ = [
  Assert<Equal<
    IsUppercase<'SUNSHINE'>, true
  >>,
  Assert<Equal<
    IsUppercase<'SUNSHINe'>, false
  >>,
];

38.3.2 Example: ToString

ToString uses normal template literal type interpolation to convert primitive literal types to string literal types:

type ToString<
  T extends string | number | bigint | boolean | null | undefined
> = `${T}`
;

type _ = Assert<Equal<
  ToString<'abc' | 123 | -456n | false | null | undefined>,
  'abc' | '123' | '-456' | 'false' | 'null' | 'undefined'
>>;

38.3.3 Example: trimming string literal types

To trim a string literal type, we recursively remove all spaces from its start and its end:

type TrimStart<Str extends string> =
  Str extends ` ${infer Rest}`
    ? TrimStart<Rest> // (A)
    : Str
type TrimEnd<Str extends string> =
  Str extends `${infer Rest} `
    ? TrimEnd<Rest> // (B)
    : Str
;
type Trim<Str extends string> = TrimStart<TrimEnd<Str>>;
type _ = [
  Assert<Equal<
    TrimStart<'  text  '>,
    'text  '
  >>,
  Assert<Equal<
    TrimEnd<'  text  '>,
    '  text'
  >>,
  Assert<Equal<
    Trim<'  text  '>,
    'text'
  >>,
];

Note the self-recursive invocations in line A and line B.

Acknowledgement: This example was inspired by code by Mike Ryan.

38.4 Working with tuples

38.4.1 Joining a tuple of strings

type Join<Strs extends string[], Sep extends string = ','> =
  Strs extends [
    infer First extends string,
    ...infer Rest extends string[]
  ]
    ? Rest['length'] extends 0
      ? First
      : `${First}${Sep}${Join<Rest, Sep>}`
    : ''
;
type _ = Assert<Equal<
  Join<['Hello', 'How', 'Are', 'You'], ' '>,
  'Hello How Are You'
>>;

Once again, we are programming at a type level in a functional style.

In the second line, we take the tuple Strs apart (think destructuring):

Next:

38.4.2 Splitting a string

type Split<Str extends string, Sep extends string> =
  Str extends `${infer First}${Sep}${infer Rest}`
    ? [First, ...Split<Rest, Sep>] // (A)
    : [Str] // (B)
;
type _ = Assert<Equal<
  Split<'How | are|you', '|'>,
  ['How ', ' are', 'you']
>>;

We use infer inside a template literal to determine the prefix First before a separator Sep and then recursively invoke Split (line A).

If no separator is found, the result is a tuple with the whole Str (line B).

38.4.3 Example: defining a string literal union type via a string literal type

We can use the previously defined Split and Trim to convert a string literal type to a string literal union type:

type StringToUnion<Str extends string> =
  Trim<TupleToUnion<Split<Str, '|'>>>;
type TupleToUnion<Tup extends readonly unknown[]> = Tup[number]; 

type _ = Assert<Equal<
  StringToUnion<'A | B | C'>,
  'A' | 'C' | 'B'
>>;

TupleToUnion treats the tuple Tup as a map from numbers (indices) to values: Tup[number] means “give me all the values” (the range of Tup).

Note how the application of Trim is distributed and applied to each member of the union.

38.4.4 Splitting a string into code units

If we infer without a fixed separator, then the First inferred string is always a single code unit (a “JavaScript character”, not a Unicode code point which comprises one or two code units):

type SplitCodeUnits<Str extends string> =
  Str extends `${infer First}${infer Rest}`
    // `First` is not empty
    ? [First, ...SplitCodeUnits<Rest>]
    // `First` (and therefore `Str`) is empty
    : []
;
type _ = [
  Assert<Equal<
    SplitCodeUnits<'rainbow'>,
    ['r', 'a', 'i', 'n', 'b', 'o', 'w']
  >>,
  Assert<Equal<
    SplitCodeUnits<''>,
    []
  >>,
];

38.5 Working with objects

38.5.1 Example: adding prefixes to property names

Below, we are using a mapped type to convert an object type Obj to a new object type, where each property key starts with a dollar sign ($):

type PrependDollarSign<Obj> = {
  [Key in (keyof Obj & string) as `$${Key}`]: Obj[Key]
};

type Items = {
  count: number,
  item: string,
  [Symbol.toStringTag]: string,
};
type _ = Assert<Equal<
  PrependDollarSign<Items>,
  {
    $count: number,
    $item: string,
    // Omitted: [Symbol.toStringTag]
  }
>>;

How does this code work? We assemble an output object like this:

38.6 Practical examples

38.6.1 Example: styling output to a terminal

In Node.js, we can use util.styleText() to log styled text to the console:

console.log(
  util.styleText(['bold', 'underline', 'red'], 'Hello!')
);

To get a list of all possible style values, evaluate this expression in the Node.js REPL:

Object.keys(util.inspect.colors)

Below, we define a function styleText() that uses a single string to specify multiple styles and statically checks that that string has the proper format:

const styles = [
  'bold',
  'italic',
  'underline',
  'red',
  'green',
  'blue',
  // ...
] as const; // `as const` enables us to derive a type
type StyleUnion = TupleToUnion<typeof styles>;
type TupleToUnion<Tup extends readonly unknown[]> = Tup[number]; 

type StyleTextFormat =
| `${StyleUnion}`
| `${StyleUnion}+${StyleUnion}`
| `${StyleUnion}+${StyleUnion}+${StyleUnion}`
;

function styleText(format: StyleTextFormat, text: string): string {
  return util.styleText(format.split('+'), text);
}

styleText('bold+underline+red', 'Hello!'); // OK
// @ts-expect-error: Argument of type '"bol+underline+red"' is not
// assignable to parameter of type 'StyleTextFormat'.
styleText('bol+underline+red', 'Hello!'); // typo: 'bol'

38.6.2 Example: property paths

The following example is based on code by Anders Hejlsberg:

type PropType<T, Path extends string> =
  Path extends keyof T
    // `Path` is already a key of `T`
    ? T[Path]
    // Otherwise: extract first dot-separated key
    : Path extends `${infer First}.${infer Rest}`
      ? First extends keyof T
        // Use key `First` and compute PropType for result
        ? PropType<T[First], Rest>
        : unknown
      : unknown;

function getPropValue
  <T, P extends string>
  (value: T, path: P): PropType<T, P>
{
  // Not implemented yet...
  return null as PropType<T, P>;
}

const obj = { a: { b: ['x', 'y']}} as const;

assertType<
  { readonly b: readonly ['x', 'y'] }
>(getPropValue(obj, 'a'));

assertType<
  readonly ['x', 'y']
>(getPropValue(obj, 'a.b'));

assertType<
  'y'
>(getPropValue(obj, 'a.b.1'));

assertType<
  unknown
>(getPropValue(obj, 'a.b.p'));

function myFunc(str: string) {
  // If the second argument is not a literal,
  // we can’t infer a return type.
  assertType<
    unknown
  >(getPropValue(obj, str));
}

38.6.3 Example: changing property name prefixes

This is a more complicated version of an earlier example:

type PropKeysAtToUnderscore<Obj> = {
  [Key in keyof Obj as AtToUnderscore<Key>]: Obj[Key];
};
type AtToUnderscore<Key> =
  // Remove prefix '@', add prefix '_'
  Key extends `@${infer Rest}` ? `_${Rest}` : Key // (A)
;

type JsonLd = {
  '@context': string,
  '@type': string,
  datePublished: string,
};
type _ = Assert<Equal<
  PropKeysAtToUnderscore<JsonLd>,
  {
    _context: string,
    _type: string,
    datePublished: string,
  }
>>;

Note that in line A, we only the change key if it is a string that starts with an at symbol. Other keys (including symbols) are not changed.

We could constrain the type of Key like this:

AtToUnderscore<Key extends string>

But then we couldn’t use AtToUnderscore for symbols and would have to filter values in some manner before passing them to this utility type.

38.6.4 Example: converting camel case to hyphen case

We can use template literal types to convert a string in camel case (JavaScript) to one in hyphen case (CSS).

Let’s first convert a string to a tuple of strings:

type SplitCamelCase<
  Str extends string,
  Word extends string = '',
  Words extends string[] = []
> = Str extends `${infer Char}${infer Rest}`
  ? IsUppercase<Char> extends true
    // `Word` is empty if initial `Str` starts with capital letter
    ? SplitCamelCase<Rest, Char, Append<Words, Word>>
    : SplitCamelCase<Rest, `${Word}${Char}`, Words>
  // We have reached the end of `Str`:
  // `Word` is only empty if initial `Str` was empty.
  : [...Words, Word]
;
type IsUppercase<Str extends string> = Str extends Uppercase<Str>
  ? true
  : false
;
// Only append `Str` to `Arr` if `Str` isn’t empty
type Append<Arr extends string[], Str extends string> =
  Str extends ''
    ? Arr
    : [...Arr, Str]
;

type _1 = [
  Assert<Equal<
    SplitCamelCase<'howAreYou'>,
    ['how', 'Are', 'You']
  >>,
  Assert<Equal<
    SplitCamelCase<'PascalCase'>,
    ['Pascal', 'Case']
  >>,
  Assert<Equal<
    SplitCamelCase<'CAPS'>,
    ['C', 'A', 'P', 'S']
  >>,
];

To understand how this works, consider the roles of the parameters:

The rest of the work involves lower-casing and joining the tuple elements, with hyphens as separators:

type ToHyphenCase<Str extends string> =
  HyphenateWords<SplitCamelCase<Str>>
;
type HyphenateWords<Words extends string[]> =
  Words extends [
    infer First extends string,
    ...infer Rest extends string[]
  ]
    ? Rest['length'] extends 0
      ? Lowercase<First>
      : `${Lowercase<First>}-${HyphenateWords<Rest>}`
    : ''
;

type _2 = [
  Assert<Equal<
    ToHyphenCase<'howAreYou'>,
    'how-are-you'
  >>,
  Assert<Equal<
    ToHyphenCase<'PascalCase'>,
    'pascal-case'
  >>,
];

38.6.5 Example: converting hyphen case to camel case

Going the opposite way, from hyphen case to camel case, is easier because we have hyphens as separators. As a utility type, we use Split from earlier in this chapter.

type ToLowerCamelCase<Str extends string> =
  Uncapitalize<ToUpperCamelCase<Str>>
;
// Upper camel case (Pascal case) is easier to compute
type ToUpperCamelCase<Str extends string> =
  camelizeWords<Split<Str, '-'>>
;

type camelizeWords<Words extends string[]> =
  Words extends [
    infer First extends string,
    ...infer Rest extends string[]
  ]
    ? Rest['length'] extends 0
      ? Capitalize<First>
      : `${Capitalize<First>}${camelizeWords<Rest>}`
    : ''
;

type _ = [
  Assert<Equal<
    ToLowerCamelCase<'how-are-you'>,
    'howAreYou'
  >>,
  Assert<Equal<
    ToUpperCamelCase<'how-are-you'>,
    'HowAreYou'
  >>,
];

38.7 Neat things people are doing with template literal types

In this section, I have collected interesting things I have seen people do with template literal types.

38.7.1 Node.js: type for UUIDs

@types/node uses the following type for UUIDs:

type UUID = `${string}-${string}-${string}-${string}-${string}`;

38.7.2 Parsing CLI arguments (Stefan Baumgartner)

Given the following definitions for the parameters of a shell script (num is an arbitrary name for a string parameter – not a type):

const opts = program
  .option("-e, --episode <num>", "Download episode No. <num>")
  .option("--keep", "Keeps temporary files")
  .option("--ratio [ratio]", "Either 16:9, or a custom ratio")
  .opts();

This type can be statically derived for opts:

{
  episode: string;
} & {
  keep: boolean;
} & {
  ratio: string | boolean;
}

More information: “The TypeScript converging point” by Stefan Baumgartner

38.7.3 Smart result type of document.querySelector() (Mike Ryan)

By parsing the argument of querySelector() at compile time, we can derive nice types:

const a = querySelector('div.banner > a.call-to-action');
  // HTMLAnchorElement
const b = querySelector('input, div');
  // HTMLInputElement | HTMLDivElement
const c = querySelector('circle[cx="150"]');
  // SVGCircleElement
const d = querySelector('button#buy-now');
  // HTMLButtonElement
const e = querySelector('section p:first-of-type');
  // HTMLParagraphElement

More information: tweet by Mike Ryan.

38.7.4 Typing routes in Angular (Mike Ryan)

const AppRoutes = routes(
  {
    path: '' as const,
  },
  {
    path: 'book/:id' as const,
    children: routes(
      {
        path: 'author/:id' as const,
      },
    ),
  },
);

// `AppRoutes` determines (statically!) what arguments can be
// passed to `buildPath()`:

const buildPath = createPathBuilder(AppRoutes);
buildPath(); // OK
buildPath('book', 12); // OK
buildPath('book', '123', 'author', 976); // OK

buildPath('book', null);
   // Error: argument not assignable to parameter
buildPath('fake', 'route');
   // Error: argument not assignable to parameter

More information: tweet by Mike Ryan.

38.7.5 Express route extractor (Dan Vanderkam)

The first argument of handleGet() determines the parameters of the callback:

handleGet(
  '/posts/:postId/:commentId',
  ({postId, commentId}) => {
    console.log(postId, commentId);
  }
);

More information: tweet by Dan Vanderkam

38.7.6 Tailwind color variations (Tomek Sułkowski)

Thanks to template literals being distributive, we can define TailwindColor very concisely:

type BaseColor =
  'gray' | 'red' | 'yellow' | 'green' |
  'blue' | 'indigo' | 'purple' | 'pink';
type Variant =
  50 | 100 | 200 | 300 | 400
  500 | 600 | 700 | 800 | 900;
type TailwindColor = `${BaseColor}-${Variant}`;

More information: tweet by Tomek Sułkowski

38.7.7 Arktype: defining types

Where the TypeScript library Zod uses chained method calls to define types, the ArkType library uses (mostly) string literals that are parsed via template literal types. I prefer Zod’s approach, but it’s amazing how much ArkType does with string literals:

const currentTsSyntax = type({
  keyword: "null",
  stringLiteral: "'TS'",
  numberLiteral: "5",
  bigintLiteral: "5n",
  union: "string|number",
  intersection: "boolean&true",
  array: "Date[]",
  grouping: "(0|1)[]",
  objectLiteral: {
    nested: "string",
    "optional?": "number"
  },
  tuple: ["number", "number"]
});

38.8 Conclusion and caveats

It’s amazing what people are doing with template literal types: We now can statically check complex data in string literals or use them to derive types. However, doing so also comes with caveats:

38.9 Further reading

38.10 Sources of this chapter