🔷 TypeScript Q47 / 101

What is template literal types?

AI-Powered Answer ✓ Answered

Template Literal Types are a powerful feature introduced in TypeScript 4.1 that allow you to create new string literal types by concatenating other string literal types, primitive types, and union types. They behave similarly to template literals in JavaScript, but at the type level, enabling rich string manipulation and pattern matching within the type system.

What are Template Literal Types?

At their core, Template Literal Types allow you to express string patterns in types. You can use them to define a new type that is a combination of fixed string parts and other types (like string literals, union types, or even string itself). When evaluated, TypeScript will try to resolve all possible string combinations represented by the template, resulting in a union of string literal types.

Syntax

The syntax for Template Literal Types mimics JavaScript's template literals, using backticks (`) and placeholders (embedded expressions) with the ${Type} syntax.

typescript
type World = 'world';
type Greeting = `hello ${World}`;

// Greeting is equivalent to type Greeting = 'hello world';

type Color = 'red' | 'green' | 'blue';
type Direction = 'up' | 'down';

type Action = `move${Capitalize<Direction>}`;
// Action is 'moveUp' | 'moveDown'

type Status = `${Color}Light`;
// Status is 'redLight' | 'greenLight' | 'blueLight'

Key Type Modifiers

Template Literal Types often work in conjunction with several utility types to modify casing:

  • Uppercase<StringType>: Converts all string literal characters to uppercase.
  • Lowercase<StringType>: Converts all string literal characters to lowercase.
  • Capitalize<StringType>: Converts the first character of the string literal to uppercase.
  • Uncapitalize<StringType>: Converts the first character of the string literal to lowercase.
typescript
type EventName = `on${Capitalize<Color>}Change`;
// EventName is 'onRedChange' | 'onGreenChange' | 'onBlueChange'

type Prefix = 'foo';
type Suffix = 'BAR';
type Combined = `${Lowercase<Prefix>}${Uppercase<Suffix>}`;
// Combined is 'fooBAR'

Common Use Cases

  • Creating Type-Safe Event Names: Generate specific event strings like 'onChange', 'onToggle', etc.
  • API Endpoint Generation: Construct precise API path types, e.g., /api/users/${ID}.
  • CSS Utility Class Generation: Create types for classes like 'text-red-500', 'bg-blue-100'.
  • Property Name Transformation: Map property names, for example, converting 'name' to 'getters.name' or 'setName'.
  • String Manipulation within Types: Perform advanced string transformations to derive new types from existing ones.
  • Improving Autocompletion: By narrowing down possible string values, they significantly enhance IDE autocompletion.

Example: Building a 'Prop' Accessor Type

Let's say you have an object and want to create a type that represents a string path to its properties. Template Literal Types can make this type-safe.

typescript
type Primitive = string | number | boolean | undefined | null;

type Join<K, P> = K extends string | number ?
    P extends string | number ?
        `${K}.${P}`
        : never
    : never;

// Recursive type to generate all possible property paths
type DeepKeys<T> = T extends Primitive ? never :
    {
        [K in keyof T]: K extends string | number ?
            T[K] extends Primitive ?
                K
                : K | Join<K, DeepKeys<T[K]>>
            : never
    }[keyof T];

interface UserProfile {
    id: string;
    name: {
        first: string;
        last: string;
    };
    contact: {
        email: string;
        phone?: string;
    };
    isActive: boolean;
}

type UserProfilePaths = DeepKeys<UserProfile>;
/*
UserProfilePaths is now:
"id" |
"name" | "name.first" | "name.last" |
"contact" | "contact.email" | "contact.phone" |
"isActive"
*/

function getProp<T, K extends DeepKeys<T>>(obj: T, path: K): any {
    // A simplified runtime implementation (actual implementation would be more robust)
    const parts = (path as string).split('.');
    let current: any = obj;
    for (const part of parts) {
        if (current === null || typeof current !== 'object' || !(part in current)) {
            return undefined;
        }
        current = current[part];
    }
    return current;
}

const user: UserProfile = {
    id: '123',
    name: { first: 'John', last: 'Doe' },
    contact: { email: 'john.doe@example.com' },
    isActive: true
};

const firstName = getProp(user, 'name.first'); // 'John'
const email = getProp(user, 'contact.email');   // 'john.doe@example.com'
// const invalidPath = getProp(user, 'name.middle'); // Type error: Argument of type '"name.middle"' is not assignable to parameter of type 'UserProfilePaths'

Conclusion

Template Literal Types significantly enhance TypeScript's ability to work with string patterns and provide a powerful mechanism for type-level string manipulation. They allow for more precise types, better autocompletion, and safer code by enforcing specific string formats, leading to a more robust and enjoyable developer experience.