Polyglot
Languages TypeScript advanced types
TypeScript § advanced-types

Advanced types

TypeScript’s advanced types admit substantial type-level computation: mapped types (transforming property types across a key set), conditional types (T extends X ? Y : Z), template literal types (string-typed pattern construction), the infer keyword (extracting types from positions), and the utility types (Partial, Required, Pick, Omit, Record, ReturnType, Awaited, etc.). The combination admits expressing substantial type-level logic — the type system is itself a small functional programming language operating on types. The conventional discipline is to lean on the standard utility types for routine transformations and to reserve elaborate conditional/mapped types for genuinely generic library code.

Mapped types

A mapped type admits transforming the keys and values of an existing type:

type Optional<T> = {
    [K in keyof T]?: T[K];
};

interface Person {
    name: string;
    age: number;
    email: string;
}

type OptionalPerson = Optional<Person>;
// {
//     name?: string | undefined;
//     age?: number | undefined;
//     email?: string | undefined;
// }

The form [K in keyof T]: ... iterates over the keys of T, producing a property for each key. The mechanism admits substantial structural transformations.

Mapping modifiers

The ? and readonly modifiers admit adding or removing optionality and immutability:

// Add optional and readonly:
type ReadonlyOptional<T> = {
    readonly [K in keyof T]?: T[K];
};

// Remove optional (`-?`) and readonly (`-readonly`):
type Required<T> = {
    [K in keyof T]-?: T[K];
};

type Mutable<T> = {
    -readonly [K in keyof T]: T[K];
};

The -? and -readonly admit “remove this modifier”; the bare ? and readonly admit “add this modifier”.

Key remapping with as

Since TS 4.1, mapped types admit key remapping via as:

type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person {
    name: string;
    age: number;
}

type PersonGetters = Getters<Person>;
// {
//     getName: () => string;
//     getAge: () => number;
// }

The mechanism admits substantial key transformation; conventional for builder-pattern types and DSLs.

For filtering keys, as with a conditional admits “include/exclude”:

type FilterKeys<T, U> = {
    [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface User {
    name: string;
    age: number;
    email: string;
    active: boolean;
}

type StringKeys = FilterKeys<User, string>;
// { name: string; email: string }

The K in keyof T as ... ? K : never admits “keep K if condition; drop otherwise” — a key with type never is excluded from the resulting type.

Conditional types

The form T extends X ? Y : Z admits type-level branching:

type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;                      // true
type B = IsString<42>;                           // false

Distributive conditional types

Conditional types are distributive over unions when the checked type is a “naked” type parameter:

type ExtractString<T> = T extends string ? T : never;

type A = ExtractString<string | number | boolean>;   // string
// Distributes: (string extends string ? string : never)
//            | (number extends string ? number : never)
//            | (boolean extends string ? boolean : never)
//            = string | never | never
//            = string

To prevent distribution, wrap the parameter in a tuple:

type IsUnion<T> = [T] extends [string] ? true : false;

type A = IsUnion<string>;                        // true
type B = IsUnion<string | number>;               // false (the tuple form does not distribute)

The mechanism is conventional for substantial type-level logic.

Extract and Exclude

The standard library’s distributive utilities:

type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;

type A = Extract<"a" | "b" | "c", "a" | "c">;    // "a" | "c"
type B = Exclude<"a" | "b" | "c", "a">;          // "b" | "c"

type C = Extract<string | number, string>;       // string
type D = Exclude<string | number, string>;       // number

The infer keyword

infer T introduces a type variable in a conditional’s extends clause:

type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never;

type A = ReturnType<() => string>;               // string
type B = ReturnType<(x: number) => boolean>;     // boolean

type Parameters<F> = F extends (...args: infer P) => any ? P : never;

type C = Parameters<(x: number, y: string) => void>;  // [number, string]

type ArrayElement<A> = A extends (infer T)[] ? T : never;

type D = ArrayElement<string[]>;                  // string
type E = ArrayElement<number[][]>;                // number[]

The infer admits extracting a type from a positional template. The mechanism is one of TypeScript’s most powerful type-level operators.

Awaited

The standard Awaited<T> recursively unwraps promises:

type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

type A = Awaited<Promise<string>>;               // string
type B = Awaited<Promise<Promise<number>>>;      // number
type C = Awaited<string>;                        // string

Template literal types

Since TS 4.1, template literal types admit pattern construction at the type level:

type Greeting = `Hello, ${string}!`;
type Specific = "Hello, world!";

const a: Greeting = "Hello, world!";             // OK
const b: Greeting = "Hi, there!";                // ERROR

type Direction = `${"north" | "south"}-${"east" | "west"}`;
// "north-east" | "north-west" | "south-east" | "south-west"

type CSSValue = `${number}px` | `${number}em` | `${number}%`;

const v: CSSValue = "100px";                     // OK
const w: CSSValue = "100";                       // ERROR

The mechanism admits substantial precision for string-formatted types — CSS values, route parameters, event names, etc.

Template literal type extraction

type ExtractParam<T> = T extends `${string}{${infer P}}${string}` ? P : never;

type A = ExtractParam<"GET /users/{id}">;        // "id"
type B = ExtractParam<"POST /posts/{postId}/comments/{commentId}">;  // "commentId"
                                                  // (only the last; needs recursion for all)

// Recursive extraction:
type ExtractAllParams<T> =
    T extends `${string}{${infer P}}${infer Rest}`
        ? P | ExtractAllParams<Rest>
        : never;

type C = ExtractAllParams<"/users/{id}/posts/{postId}">;
// "id" | "postId"

The pattern is conventional in route-typing and template engines.

Built-in template-string utilities

type A = Uppercase<"hello">;                     // "HELLO"
type B = Lowercase<"HELLO">;                     // "hello"
type C = Capitalize<"hello">;                    // "Hello"
type D = Uncapitalize<"Hello">;                  // "hello"

The standard utility types

TypeScript provides substantial utility types for common transformations:

Partial<T> — make all properties optional

interface User {
    name: string;
    age: number;
}

type PartialUser = Partial<User>;
// { name?: string; age?: number }

function update(user: User, changes: Partial<User>): User {
    return { ...user, ...changes };
}

Required<T> — make all properties required

interface Config {
    host?: string;
    port?: number;
}

type StrictConfig = Required<Config>;
// { host: string; port: number }

Readonly<T> — make all properties readonly

interface User {
    name: string;
    age: number;
}

type ImmutableUser = Readonly<User>;
// { readonly name: string; readonly age: number }

Pick<T, K> — select keys

interface User {
    name: string;
    age: number;
    email: string;
    password: string;
}

type PublicUser = Pick<User, "name" | "email">;
// { name: string; email: string }

Omit<T, K> — exclude keys

type SafeUser = Omit<User, "password">;
// { name: string; age: number; email: string }

Record<K, V> — typed record

type Permissions = Record<string, boolean>;
type Counts = Record<"active" | "inactive", number>;

const counts: Counts = {
    active: 10,
    inactive: 5,
};

Exclude<T, U> and Extract<T, U>

type A = Exclude<"a" | "b" | "c", "a">;          // "b" | "c"
type B = Extract<string | number, string>;       // string

NonNullable<T> — exclude null and undefined

type A = NonNullable<string | null | undefined>;  // string

ReturnType<F> and Parameters<F>

function fn(x: number, y: string): boolean { /* ... */ }

type R = ReturnType<typeof fn>;                  // boolean
type P = Parameters<typeof fn>;                  // [number, string]

Awaited<T> — unwrap promises

type A = Awaited<Promise<string>>;               // string
type B = Awaited<Promise<Promise<number>>>;      // number

ConstructorParameters<C> and InstanceType<C>

class User {
    constructor(public name: string, public age: number) {}
}

type Args = ConstructorParameters<typeof User>;   // [name: string, age: number]
type Instance = InstanceType<typeof User>;       // User

Common patterns

Deep partial

type DeepPartial<T> = T extends object
    ? { [K in keyof T]?: DeepPartial<T[K]> }
    : T;

interface Config {
    server: {
        host: string;
        port: number;
        ssl: { cert: string; key: string };
    };
}

type ConfigPatch = DeepPartial<Config>;
// All properties at all depths are optional.

Deep readonly

type DeepReadonly<T> = T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;

Function type extraction

type FnReturn<F> = F extends (...args: any[]) => infer R ? R : never;

interface Service {
    getUser: (id: string) => Promise<User>;
    createPost: (data: PostData) => Promise<Post>;
}

type GetUserResult = FnReturn<Service["getUser"]>;  // Promise<User>

Discriminated union to map

type ActionMap<A extends { type: string }> = {
    [K in A["type"]]: Extract<A, { type: K }>;
};

type Action =
    | { type: "INCREMENT"; by: number }
    | { type: "DECREMENT"; by: number }
    | { type: "RESET" };

type ByType = ActionMap<Action>;
// {
//     INCREMENT: { type: "INCREMENT"; by: number };
//     DECREMENT: { type: "DECREMENT"; by: number };
//     RESET: { type: "RESET" };
// }

Type-safe event emitter

type EventMap = {
    click: { x: number; y: number };
    keypress: { key: string };
    scroll: { offset: number };
};

class TypedEmitter<E extends Record<string, any>> {
    on<K extends keyof E>(event: K, handler: (data: E[K]) => void): void { /* ... */ }
    emit<K extends keyof E>(event: K, data: E[K]): void { /* ... */ }
}

const emitter = new TypedEmitter<EventMap>();
emitter.on("click", e => console.log(e.x, e.y));  // OK
emitter.emit("click", { x: 1, y: 2 });            // OK
// emitter.emit("click", { x: 1 });               // ERROR: missing y

Object key transformation

type CamelToSnake<S extends string> =
    S extends `${infer Head}${infer Tail}`
        ? Head extends Lowercase<Head>
            ? `${Head}${CamelToSnake<Tail>}`
            : `_${Lowercase<Head>}${CamelToSnake<Tail>}`
        : S;

type SnakeKeys<T> = {
    [K in keyof T as K extends string ? CamelToSnake<K> : K]: T[K];
};

interface User {
    firstName: string;
    lastName: string;
    emailAddress: string;
}

type SnakeUser = SnakeKeys<User>;
// {
//     first_name: string;
//     last_name: string;
//     email_address: string;
// }

Pick by value type

type PickByValue<T, V> = {
    [K in keyof T as T[K] extends V ? K : never]: T[K];
};

interface Mixed {
    a: string;
    b: number;
    c: string;
    d: boolean;
}

type Strings = PickByValue<Mixed, string>;
// { a: string; c: string }

Merge two types

type Merge<A, B> = Omit<A, keyof B> & B;

interface Defaults {
    timeout: number;
    retries: number;
    verbose: boolean;
}

interface Overrides {
    timeout: number;
    apiKey: string;
}

type Config = Merge<Defaults, Overrides>;
// { retries: number; verbose: boolean; timeout: number; apiKey: string }

Tuple type manipulation

type Head<T extends readonly unknown[]> = T extends readonly [infer H, ...unknown[]] ? H : never;
type Tail<T extends readonly unknown[]> = T extends readonly [unknown, ...infer R] ? R : [];

type A = Head<[1, 2, 3]>;                        // 1
type B = Tail<[1, 2, 3]>;                        // [2, 3]

Conditional return type

function find<T, K extends keyof T>(
    items: readonly T[],
    key: K,
    value: T[K] | null,
): T | undefined {
    return items.find(item => item[key] === value);
}

Recursive type for JSON

type JsonValue =
    | string
    | number
    | boolean
    | null
    | JsonValue[]
    | { [key: string]: JsonValue };

type JsonObject = { [key: string]: JsonValue };
type JsonArray = JsonValue[];

Brand types

type Brand<T, B> = T & { readonly __brand: B };

type UserId = Brand<number, "UserId">;
type GroupId = Brand<number, "GroupId">;

function makeUserId(n: number): UserId { return n as UserId; }
function makeGroupId(n: number): GroupId { return n as GroupId; }

const u = makeUserId(42);
const g = makeGroupId(99);

function lookup(id: UserId) { /* ... */ }
lookup(u);                                        // OK
// lookup(g);                                      // ERROR
// lookup(42);                                     // ERROR

A note on the conventional discipline

The contemporary TypeScript advanced types advice:

  • Use the standard utility types — they cover most routine cases.
  • Use mapped types for structural transformations.
  • Use conditional types sparingly — for genuinely generic library code.
  • Use infer in conditional types for type extraction.
  • Use template literal types for substantial string-pattern types.
  • Use brand types for nominal type safety.
  • Avoid over-engineering — explicit types are conventionally clearer than substantial type-level computation.
  • Trust the inference — explicit type arguments are usually unnecessary.

The combination — mapped types, conditional types, the infer keyword, template literal types, and the standard utility types — is the substance of TypeScript’s advanced type surface. The discipline produces substantial expressiveness; the trade-off is the type-level code’s relative opacity. The conventional advice is to write plain types where possible and reach for the advanced surface only when type-level computation is genuinely needed.