Polyglot
Languages TypeScript generics
TypeScript § generics

Generics

Generics admit writing functions, classes, and types parameterised by other types. TypeScript’s generics are substantial: type parameters with constraints, default type arguments, generic functions and classes, generic interfaces and type aliases, conditional types, and the infer keyword. The combination — type parameters at almost every position, structural compatibility, conditional inference — admits substantial type-level computation. The conventional discipline is to use generics for genuinely generic code and to allow inference where possible (the compiler is substantial at inferring type arguments).

Generic functions

Type parameters appear in angle brackets after the function name:

function identity<T>(x: T): T {
    return x;
}

const a = identity<number>(42);                  // explicit
const b = identity(42);                          // T inferred as number
const c = identity("hello");                     // T inferred as string

The form: function name<T>(params): returnType { body }. Each type parameter has a name (conventionally T, U, V, or descriptive names like Item, Key).

For arrow functions, the form is the same:

const identity = <T>(x: T): T => x;

In .tsx files, the trailing comma is needed to disambiguate from JSX:

const identity = <T,>(x: T): T => x;

Multiple type parameters

function pair<A, B>(a: A, b: B): [A, B] {
    return [a, b];
}

const p = pair("hello", 42);                     // [string, number]

function map<T, U>(arr: T[], f: (x: T) => U): U[] {
    return arr.map(f);
}

const lengths = map(["a", "bb", "ccc"], s => s.length);
// lengths: number[]

Generic constraints

The extends keyword admits constraining a type parameter:

function longest<T extends { length: number }>(a: T, b: T): T {
    return a.length >= b.length ? a : b;
}

longest("hello", "hi");                          // OK (string has length)
longest([1, 2, 3], [4, 5]);                      // OK (array has length)
// longest(1, 2);                                 // ERROR: number lacks length

The constraint admits using the constrained operations on the parameter:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name");          // type: string
const age = getProperty(user, "age");            // type: number
// const x = getProperty(user, "email");         // ERROR: "email" not a key

The pattern is one of TypeScript’s most distinctive idioms; treated in Advanced types.

Default type arguments

Type parameters may have defaults:

interface Container<T = string> {
    value: T;
    serialize(): string;
}

const a: Container = { value: "hello", serialize: () => "..." };           // T = string
const b: Container<number> = { value: 42, serialize: () => "..." };

function fetch<T = unknown>(url: string): Promise<T> {
    return /* ... */;
}

const data1 = await fetch("/api");                // Promise<unknown>
const data2 = await fetch<User>("/api/user");      // Promise<User>

The mechanism admits backward-compatible additions and conventional sensible defaults.

Generic types

Type aliases and interfaces admit generics:

type Pair<A, B> = [A, B];
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };

interface List<T> {
    head: T;
    tail: List<T> | null;
}

interface Map<K, V> {
    get(key: K): V | undefined;
    set(key: K, value: V): void;
}

Instantiation requires the type arguments:

const p: Pair<string, number> = ["alice", 30];
const r: Result<number> = { ok: true, value: 42 };

Generic classes

class Stack<T> {
    private items: T[] = [];

    push(x: T): void {
        this.items.push(x);
    }

    pop(): T | undefined {
        return this.items.pop();
    }

    peek(): T | undefined {
        return this.items[this.items.length - 1];
    }
}

const s = new Stack<number>();
s.push(1);
s.push(2);
const top = s.pop();                             // type: number | undefined

const t = new Stack<string>();
t.push("hello");

Methods may have additional type parameters:

class Container<T> {
    private value: T;
    constructor(value: T) { this.value = value; }

    map<U>(fn: (x: T) => U): Container<U> {
        return new Container(fn(this.value));
    }

    get(): T { return this.value; }
}

const c = new Container(42);
const c2 = c.map(n => n.toString());             // Container<string>

Generic interfaces

interface Pair<A, B> {
    first: A;
    second: B;
}

interface Iterator<T> {
    next(): { value: T; done: boolean };
}

interface Comparator<T> {
    (a: T, b: T): number;
}

The conventional standard interfaces (Array<T>, Promise<T>, Map<K, V>, etc.) are generic.

Type inference

The compiler is substantial at inferring type arguments:

function map<T, U>(arr: T[], f: (x: T) => U): U[] { /* ... */ }

const result = map([1, 2, 3], n => n.toString());
//          ^^ type: string[]
//                            ^^ T inferred as number
//                            ^^ U inferred as string

When inference fails or is incorrect, explicit arguments are admitted:

const explicit = map<number, string>([1, 2, 3], n => n.toString());

The conventional discipline lets the compiler infer when possible; explicit arguments are reserved for cases where inference is wrong or missing.

Conditional types

Since TS 2.8, conditional types admit type-level branching:

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

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

The form T extends X ? Y : Z produces Y if T is assignable to X, otherwise Z. The mechanism admits substantial type-level logic.

A common pattern: extracting a type from a union:

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

type A = ExtractString<string | number | boolean>;   // string

// Built-in:
type B = Extract<string | number | boolean, string>;  // string
type C = Exclude<string | number | boolean, string>;  // number | boolean

The infer keyword

The infer keyword (in conditional types) extracts a type from a generic position:

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

type A = ReturnType<() => string>;               // string
type B = ReturnType<(x: number) => boolean>;     // boolean
type C = ReturnType<number>;                     // never (not a function)

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

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

The mechanism is treated in Advanced types.

Variance

TypeScript’s generic types are conventionally bivariant (admit both covariant and contravariant assignments) by default, with several exceptions for type-system soundness. Since TS 4.7, explicit variance annotations are admitted:

interface ProducerOf<out T> {
    produce(): T;
}

interface ConsumerOf<in T> {
    consume(value: T): void;
}

interface BothWays<in out T> {
    transform(input: T): T;
}

The annotations document and enforce variance. The conventional discipline is to omit them unless variance correctness matters; the compiler infers reasonable variance for most cases.

Generic constraints with keyof

function pluck<T, K extends keyof T>(arr: T[], key: K): T[K][] {
    return arr.map(x => x[key]);
}

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

const users: User[] = [{ name: "Alice", age: 30 }];
const names = pluck(users, "name");              // string[]
const ages = pluck(users, "age");                // number[]
// const errors = pluck(users, "email");          // ERROR: "email" not a key

The pattern admits substantial type-safe property access.

Generic constraints with type unions

function findById<T extends { id: number | string }>(items: T[], id: T["id"]): T | undefined {
    return items.find(item => item.id === id);
}

Generic recursion

Type aliases admit recursive references (within limits):

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

const data: JsonValue = {
    name: "Alice",
    nested: [1, 2, { deep: true }],
};

Recursive types are conventional for tree structures, JSON, AST nodes, and similar.

Higher-order types

Generics admit functions that operate on other generic types:

type MapValues<T, U> = {
    [K in keyof T]: U;
};

type Lengths = MapValues<{ a: string; b: number }, number>;
// { a: number; b: number }

Treated in Advanced types.

Common patterns

Generic identity-like

function tap<T>(value: T, fn: (x: T) => void): T {
    fn(value);
    return value;
}

const x = tap(getValue(), v => console.log("got:", v));

Generic factory

function create<T>(Constructor: new () => T): T {
    return new Constructor();
}

class MyClass {}
const m = create(MyClass);                       // type: MyClass

Generic container

interface Box<T> {
    value: T;
}

function box<T>(value: T): Box<T> {
    return { value };
}

const b = box(42);                                // Box<number>
const c = box("hello");                          // Box<string>

Generic transformation

function map<T, U>(arr: readonly T[], fn: (x: T, i: number) => U): U[] {
    const result: U[] = [];
    for (let i = 0; i < arr.length; i++) {
        result.push(fn(arr[i], i));
    }
    return result;
}

const lengths = map(["a", "bb", "ccc"], s => s.length);

Generic filter

function filter<T>(arr: readonly T[], pred: (x: T) => boolean): T[] {
    return arr.filter(pred);
}

// With type predicate:
function filter<T, S extends T>(arr: readonly T[], pred: (x: T) => x is S): S[] {
    return arr.filter(pred) as S[];
}

Generic constraint with default

function fetch<T = unknown>(url: string): Promise<T> {
    return /* ... */;
}

const data: User = await fetch<User>("/api/user");

Generic union builder

type Action<T extends string, P = void> =
    P extends void ? { type: T } : { type: T; payload: P };

type IncrementAction = Action<"INCREMENT">;       // { type: "INCREMENT" }
type SetAction = Action<"SET", number>;           // { type: "SET"; payload: number }

Generic reduce

function reduce<T, U>(
    arr: readonly T[],
    initial: U,
    fn: (acc: U, x: T) => U,
): U {
    let acc = initial;
    for (const x of arr) {
        acc = fn(acc, x);
    }
    return acc;
}

const sum = reduce([1, 2, 3], 0, (acc, n) => acc + n);

Generic class with default

class EventEmitter<T = any> {
    private listeners: Array<(value: T) => void> = [];

    on(listener: (value: T) => void): void {
        this.listeners.push(listener);
    }

    emit(value: T): void {
        this.listeners.forEach(l => l(value));
    }
}

const emitter = new EventEmitter<{ name: string }>();
emitter.on(e => console.log(e.name));
emitter.emit({ name: "alice" });

Generic API client

interface ApiClient {
    get<T>(url: string): Promise<T>;
    post<T, B>(url: string, body: B): Promise<T>;
    put<T, B>(url: string, body: B): Promise<T>;
    delete(url: string): Promise<void>;
}

const user = await client.get<User>("/api/user/123");
const created = await client.post<User, { name: string }>("/api/users", { name: "alice" });

Generic Result type

type Result<T, E = Error> =
    | { ok: true; value: T }
    | { ok: false; error: E };

function ok<T>(value: T): Result<T, never> {
    return { ok: true, value };
}

function err<E>(error: E): Result<never, E> {
    return { ok: false, error };
}

function divide(a: number, b: number): Result<number, string> {
    if (b === 0) return err("division by zero");
    return ok(a / b);
}

A note on the conventional discipline

The contemporary TypeScript generics advice:

  • Use single letters (T, U, V) for short, generic-purpose parameters.
  • Use descriptive names (Item, Key) for substantial generics.
  • Use constraints (T extends ...) to enable constrained operations.
  • Use defaults for backward compatibility.
  • Trust inference — explicit type arguments are usually unnecessary.
  • Use keyof T for type-safe property access.
  • Use conditional types for type-level computation.
  • Use the standard utility typesPartial, Required, Pick, Omit, ReturnType, etc.
  • Avoid over-engineering — don’t reach for generics if a concrete type suffices.

The combination — generic functions, classes, types and interfaces, constraints, defaults, conditional types, infer — is the substance of TypeScript’s generic surface. The discipline admits substantial reuse and type-level computation; the inference is good enough that most code avoids the verbosity that other generic systems require.