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!