← thecodex.expert · The Codex Family of Knowledge
TypeScript

Advanced Generics

A generic type parameter can have a default, just like a function parameter can — letting most callers write nothing at all while still allowing full customization when needed.

TypeScript 6.0 Default type parameters Last verified:
Where this bites you

K extends keyof T with value: T[K] looks airtight, and it is, as long as K is one specific key. When the key comes from a variable typed as the whole union ("name" | "age"), K becomes that union and T[K] becomes string | number. Now any string is accepted as the value for any key, including age, and TypeScript still types the result's age as number.

TypeScriptunion_key_bug.ts
function updateProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
    return { ...obj, [key]: value };
}

const user = { name: "Alice", age: 30 };

const field: keyof typeof user = new Date().getFullYear() > 2000 ? "age" : "name";
const updated = updateProperty(user, field, "thirty-one");   // compiles

console.log(updated.age.toFixed(1));
// TypeError: updated.age.toFixed is not a function
Canonical Definition

Generics scale beyond a single type parameter: multiple parameters can reference each other (one constrained by keyof another), a type parameter can have a default so most callers never need to specify it explicitly, and a whole class can be generic, applying its type parameter consistently to every property and method inside it.

Multiple type parameters referencing each other

K is constrained to T's own keys, so TypeScript rejects a call passing a property name that doesn't actually exist on the object being updated.

TypeScriptmultiple_params.ts
function updateProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
    return { ...obj, [key]: value };
}

const user = { name: "Alice", age: 30 };
updateProperty(user, "age", 31);         // fine
// updateProperty(user, "age", "old");    // COMPILE ERROR: age must be a number
// updateProperty(user, "email", "x");     // COMPILE ERROR: email isn't a key of user

The literal-key calls above are safe because K is inferred as exactly "age" or "email". The fix for a key that comes from data is to narrow it to one key before the call. Then T[K] is precise again, and the bad call is rejected (error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.):

TypeScriptunion_key_fix.ts
const field: keyof typeof user = new Date().getFullYear() > 2000 ? "age" : "name";
if (field === "age") {
    const updated = updateProperty(user, field, 31);   // K is now exactly "age"
    console.log(updated.age.toFixed(1));               // 31.0
}

Default type parameters

Most callers can omit the type parameter entirely and get a sensible default, while advanced callers can still override it — the same tradeoff a default function parameter makes.

TypeScriptdefault_type_param.ts
interface ApiResponse<T = unknown> {
    data: T;
    status: number;
}

const generic: ApiResponse = { data: "anything", status: 200 };            // T defaults to unknown
const typed: ApiResponse<{ name: string }> = { data: { name: "Bob" }, status: 200 };  // T explicitly given

Generic classes

The type parameter is fixed once at construction and applies consistently to every method and property in the class, giving a type-safe, reusable container.

TypeScriptgeneric_class.ts
class Box<T> {
    private contents: T;

    constructor(value: T) {
        this.contents = value;
    }

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

    set(value: T): void {
        this.contents = value;
    }
}

const stringBox = new Box("hello");   // Box<string>, inferred from the constructor argument
const num: number = new Box(42).get();

Sources

1
Microsoft. TypeScript Handbook, "Generics", typescriptlang.org/docs/handbook/2/generics.html.