thecodex.expert · The Codex Family of Knowledge
TypeScript

Generics in TypeScript

Write once, type-safe for any type — generics are the tool that makes reusable TypeScript code possible.

TypeScript 5.x Structural typing Last verified:
Canonical Definition

TypeScript generics introduce type parameters (written as <T>) that make functions, interfaces, and classes work with multiple types while remaining fully type-safe — T is resolved to a concrete type at each call site, and the compiler verifies consistency throughout.

One sentence

Generics let you write code that works with multiple types while remaining type-safe — instead of writing the same function for strings, then for numbers, then for objects, you write it once with a type parameter.

Generic functions

TypeScriptgeneric_functions.ts
// Without generics — loses type information
function firstAny(arr: any[]): any {
    return arr[0];
}
const result = firstAny(["a", "b"]);  // result: any — not useful

// With generics — T is inferred from the argument
function first<T>(arr: T[]): T | undefined {
    return arr[0];
}

const s = first(["a", "b", "c"]);    // s: string | undefined — inferred!
const n = first([1, 2, 3]);          // n: number | undefined — inferred!
const x = first<boolean>([true]);    // explicit type argument

// Multiple type parameters
function zip<A, B>(a: A[], b: B[]): [A, B][] {
    return a.map((item, i) => [item, b[i]] as [A, B]);
}
zip(["a","b"], [1, 2]);   // [["a",1],["b",2]]  — type: [string,number][]

// Generic constraints — T must extend a type
function getLength<T extends { length: number }>(item: T): number {
    return item.length;    // safe because T is constrained to have length
}
getLength("hello");    // 5
getLength([1, 2, 3]);  // 3
getLength({ length: 42, name: "test" });  // 42

// keyof constraint — T[K] is the type of property K of T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}
const user = { name: "Priya", age: 28 };
getProperty(user, "name");   // "Priya" — type: string
getProperty(user, "age");    // 28      — type: number
// getProperty(user, "xyz"); // Error — "xyz" not in keyof typeof user

Generic interfaces and classes

TypeScriptgeneric_types.ts
// Generic interface
interface Repository<T> {
    findById(id: number): T | undefined;
    findAll(): T[];
    save(entity: T): T;
    delete(id: number): boolean;
}

interface User { id: number; name: string; email: string; }
interface Product { id: number; name: string; price: number; }

// Concrete implementation satisfies the generic contract
class UserRepository implements Repository<User> {
    private users: User[] = [];
    findById(id: number) { return this.users.find(u => u.id === id); }
    findAll() { return [...this.users]; }
    save(user: User) { this.users.push(user); return user; }
    delete(id: number) {
        const i = this.users.findIndex(u => u.id === id);
        if (i === -1) return false;
        this.users.splice(i, 1);
        return true;
    }
}

// Generic class
class Stack<T> {
    private items: T[] = [];
    push(item: T): void { this.items.push(item); }
    pop(): T | undefined { return this.items.pop(); }
    peek(): T | undefined { return this.items.at(-1); }
    get size(): number { return this.items.length; }
    isEmpty(): boolean { return this.items.length === 0; }
}

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

Conditional types

TypeScriptconditional_types.ts
// Conditional type — T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;   // true
type B = IsString<number>;   // false

// Infer — extract a type from within another type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Unwrap<T>     = T extends Promise<infer V> ? V : T;

// Usage
async function fetchUser(): Promise<User> { ... }
type FetchResult = Unwrap<ReturnType<typeof fetchUser>>;   // User

// Distributive conditional types — applies to each member of a union
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type F1 = Flatten<string[]>;    // string
type F2 = Flatten<number>;      // number (not an array, returns as-is)
type F3 = Flatten<string[] | number[]>;  // string | number (distributed)

// Template literal types (TypeScript 4.1+)
type Greeting = "Hello" | "Hi" | "Hey";
type Name     = "Priya" | "Rahul";
type Message  = \`\${Greeting}, \${Name}!\`;
// "Hello, Priya!" | "Hello, Rahul!" | "Hi, Priya!" | ... (6 combinations)

type EventName<T extends string> = \`on\${Capitalize<T>}\`;
type Handler = EventName<"click" | "focus">;  // "onClick" | "onFocus"

Mapped types

TypeScriptmapped_types.ts
// Mapped type — transform every property of a type
type Readonly<T> = {
    readonly [K in keyof T]: T[K];
};

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

type Nullable<T> = {
    [K in keyof T]: T[K] | null;
};

// Add/remove modifiers with + and -
type Mutable<T> = {
    -readonly [K in keyof T]: T[K];   // remove readonly
};
type Required<T> = {
    [K in keyof T]-?: T[K];           // remove optional
};

// These are actually built-in utility types — TypeScript ships them

// Re-mapping keys with as (TypeScript 4.1+)
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 }

// Filter properties by value type
type PickByValue<T, V> = {
    [K in keyof T as T[K] extends V ? K : never]: T[K];
};
type StringProps = PickByValue<Person, string>;  // { name: string }

Variance and generic constraints in depth

TypeScript uses structural typing for generics, but variance still matters. A Stack<Dog> is not assignable to Stack<Animal> if Stack is mutable (you could push a Cat into a Stack<Animal>, then pop a Dog and get a Cat). TypeScript's checker infers variance from usage — read-only positions are covariant, write-only positions are contravariant. TypeScript 4.7 added explicit variance annotations (in/out) for performance and clarity.

Commonly confused
Generic constraints (T extends U) and conditional types (T extends U ? X : Y) use the same syntax but mean different things. In function f<T extends string>, it means T must be a subtype of string (a constraint on what T can be). In type A<T> = T extends string ? "yes" : "no", it is a conditional expression evaluated at compile time for each possible T.
Type parameters are not runtime values — you cannot use them in expressions. function f<T>() { return new T(); } is invalid — T is erased at compile time. To create instances generically, pass the constructor as an argument: function f<T>(ctor: new() => T): T { return new ctor(); }.
TypeScript's ReturnType, Parameters, Awaited are built-in utility types, not runtime functions. They are resolved entirely at compile time via conditional types and infer. They produce zero JavaScript output.
How this connects
Requires first

Type erasure and the TypeScript compiler architecture

TypeScript compiles to JavaScript by stripping all type annotations — interfaces, type aliases, generic parameters, access modifiers, and the as keyword all disappear entirely. The output is valid JavaScript that runs in any ES5+ environment. This means TypeScript adds zero runtime overhead and requires no TypeScript runtime. The TypeScript compiler (tsc) has three phases: parsing (produces an AST), type checking (resolves types, reports errors), and emitting (produces JavaScript). The type checker is the most complex component — it maintains a "symbol table" mapping names to types and performs assignability, inference, and narrowing analysis.

Higher-kinded types and TypeScript's limits

TypeScript does not support higher-kinded types (HKTs) — you cannot write type Functor<F<_>> where F is itself a generic type constructor. This makes certain functional programming patterns (Functor, Monad, Applicative as type classes) difficult to express cleanly. Libraries like fp-ts work around this using "type defunctionalisation" — encoding HKTs as plain objects with a tag. This is a known limitation of TypeScript's type system, documented in the TypeScript GitHub issues.

Sources

1
TypeScript Handbook — Generics. typescriptlang.org/docs/handbook/2/generics.html.
2
TypeScript Handbook — Conditional Types. typescriptlang.org/docs/handbook/2/conditional-types.html.
3
TypeScript Handbook — Mapped Types. typescriptlang.org/docs/handbook/2/mapped-types.html.
4
TypeScript Handbook — Template Literal Types. typescriptlang.org/docs/handbook/2/template-literal-types.html.
Source confidence: High Last verified: Primary source: TypeScript Handbook · typescriptlang.org/docs/

Sources

1
TypeScript Handbook — Generics. typescriptlang.org/docs/handbook/2/generics.html.
2
TypeScript Handbook — Conditional Types. typescriptlang.org/docs/handbook/2/conditional-types.html.
3
TypeScript Handbook — Mapped Types. typescriptlang.org/docs/handbook/2/mapped-types.html.
4
TypeScript Handbook — Template Literal Types. typescriptlang.org/docs/handbook/2/template-literal-types.html.