Template Literal Types in TypeScript

March 4, 2022

TIL that TypeScript has had template literal types since TS v4.1. They work in a similar way to template literals in JavaScript — only now we interpolate (literal) types instead of expressions within the strings. The syntax is also the same:

type ShrimpType = 'pineapple' | 'lemon' | 'cocunut' | 'pepper';

// type BubbaGumpShrimp =
// "pineapple-shrimp" | "lemon-shrimp" | "cocunut-shrimp"
// | "pepper-shrimp"
type BubbaGumpShrimp = `${ShrimpType}-shrimp`;

The utility of the feature is pretty evident from the example. Now, instead of typing each type-shrimp combo, TypeScript sets the type for each string literal in the ShrimpType union.

The example also demonstrates how template literal types are built on string literal types — these are types that only represent a specific string as opposed to any string:

type Pineapple = 'pineapple';
// Type '"apple"' is not assignable to type '"pineapple"'
const pineapple: Pineapple = 'apple';

The primitive literal types that can be interpolated are the following:

  • string
  • number
  • bigint
  • boolean
  • null
  • undefined

Use of other types, such as object types will result in a TypeScript error like so:

type Foo = 'foo';

// ✅
type FooTemplateLiteral = `${Foo}`;

type Bar = {
  bar: 'bar';
};

// ❌
// Type 'Bar' is not assignable to type
// 'string | number | bigint | boolean | null | undefined'.
type BarTemplateLiteral = `${Bar}`;

Use cases

The TS docs excellently cover some common use cases and the awesome-template-literal-types repo has a bunch of interesting applications of the feature, mainly in the realm of library authoring.

One example that particularly caught my eye was Kysely, a type-safe SQL query builder library that leverages template literal types to provide the type safety it offers for queries.

I pulled out an example from the source code to show how they leverage template literal types to create a type for any property of a given database table:

/**
 * Given a database type and a union of table names in that db, returns
 * a union type with all possible `table`.`column` combinations.
 *
 * Example:
 *
 * ```ts
 * interface Person {
 *   id: number
 * }
 *
 * interface Pet {
 *   name: string
 *   species: 'cat' | 'dog'
 * }
 *
 * interface Movie {
 *   stars: number
 * }
 *
 * interface Database {
 *   person: Person
 *   pet: Pet
 *   movie: Movie
 * }
 *
 * type Columns = AnyColumnWithTable<Database, 'person' | 'pet'>
 *
 * // Columns == 'person.id' | 'pet.name' | 'pet.species'
 * ```
 */
export type AnyColumnWithTable<DB, TB extends keyof DB> = {
  [T in TB]: T extends string ? (keyof DB[T] extends string ? `${T}.${keyof DB[T]}` : never) : never;
}[TB];

As the example in the comment shows, we are able to pass the type of the database and a union of table names, and the type generation logic is able to map over all the properties of each table and add those as types using a template literal type:

`${T}.${keyof DB[T]}` // e.g. pet.name

The particular example might be hard to parse. Here’s the code with annotations to clarify what’s going on:

// TB is the table name, e.g. 'pet'
// TB extends keyof DB =
// restric the generic table type TB to DB key types
export type AnyColumnWithTable<DB, TB extends keyof DB> = {
  // [T in TB] = for each property in the table. T is table name, TB is property
  [T in TB]: T extends string // is the table name, e.g. 'pet' a string ?
    ? keyof DB[T] extends string // is table property, e.g. 'species' a string ?
      ? `${T}.${keyof DB[T]}` // if yes to both, return type as 'table.property'
      : never
    : never;
}[TB]; // use index access with table name as index to only return property types

I personal use case I ran into was needing the ability to restrict the type of a function argument to any value in a particular enum. See the contrived example below:

enum Beverage {
	Tea = 'tea'
	Coffee = 'coffee'
}

function dealWithBeverage(beverage: `${Beverage}`){ //...stuff }

// ✅
dealWithBeverage('coffee')
// ❌
dealWithBeverage('tee')

Thank you for reading and I hope that the post brought you some clarity!

Further reading