← thecodex.expert · The Codex Family of Knowledge
TypeScript

Generic Constraints

An unconstrained generic T could be anything at all — TypeScript won’t even let you access .length on it until you’ve constrained T to types that actually have one.

TypeScript 6.0 T extends SomeType Last verified:
Where this bites you

T extends { timeout: number } means "some type that at least has timeout", not "exactly { timeout: number }". Returning a hand-built default as T is therefore refused (TS2322: "'T' could be instantiated with a different subtype of constraint"). The usual reaction is to silence it with as T, and that is where the real bug starts: a caller asks for a bigger T and receives an object missing the extra fields.

TypeScriptconstraint_cast_bug.ts
function withDefaults<T extends { timeout: number }>(opts?: T): T {
    return opts ?? ({ timeout: 1000 } as T);   // silencing the error...
}

const settings = withDefaults<{ timeout: number; retries: number }>();
console.log(settings.retries);   // ...and the caller is lied to
// undefined   (typed as number)
Canonical Definition

Without a constraint, a generic type parameter T could be absolutely anything, so TypeScript only allows operations valid for every conceivable type — which in practice means almost nothing. Writing T extends SomeType restricts T to types assignable to SomeType, letting the function safely use SomeType's members on any value of type T. A common and useful constraint is keyof another generic, restricting one parameter's valid values to the property names of another.

The problem an unconstrained generic has

TypeScript has no idea T even has a .length property — T could be a number, a boolean, anything.

TypeScriptunconstrained.ts
function logLength<T>(item: T) {
    // console.log(item.length);   // COMPILE ERROR: T might not have a length property
}

Adding a constraint with extends

Now T is restricted to any type with a .length property, so accessing it is safe — while T can still be a string, an array, or any custom type that happens to have that shape.

TypeScriptconstrained.ts
interface HasLength {
    length: number;
}

function logLength<T extends HasLength>(item: T) {
    console.log(item.length);   // safe: T is guaranteed to have .length
}

logLength("hello");       // fine — string has .length
logLength([1, 2, 3]);      // fine — array has .length
// logLength(42);          // COMPILE ERROR: number doesn't have .length

keyof constraints: restricting to another type's keys

This is exactly how TypeScript's built-in Pick<T, K> and similar utility types constrain their key parameter — preventing a caller from passing a property name that doesn't actually exist on the object.

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

const user = { name: "Alice", age: 30 };
getProperty(user, "name");   // fine, returns string
// getProperty(user, "email"); // COMPILE ERROR: "email" is not a key of user

The compiler's original error was correct: the function promises whatever T the caller chooses, and no fixed default can be every possible subtype. If the function really works with one known shape, it shouldn't be generic. Take a Partial of that shape and fill in every field from complete defaults:

TypeScriptconstraint_fix.ts
type Options = { timeout: number; retries: number };

const defaults: Options = { timeout: 1000, retries: 3 };

function withDefaults(opts: Partial<Options> = {}): Options {
    return { ...defaults, ...opts };
}

const settings = withDefaults();
console.log(settings.retries);            // 3
console.log(withDefaults({ timeout: 50 }));   // { timeout: 50, retries: 3 }

Sources

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