The Generics lesson showed how a type parameter such as T lets one function work for many types. On its own, though, T could be anything, so the body cannot call .length or read .id on it. Constraints narrow what T may be, defaults let callers omit type arguments, and a few newer modifiers fine-tune inference. After this lesson you will be able to write generic code that is both flexible and fully checked.
function longest<T>(a: T, b: T): T {
return a.length >= b.length ? a : b;
// Error: Property 'length' does not exist on type 'T'
}Inside the function T is opaque. Adding extends tells the compiler the minimum shape every argument must have:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("apple", "fig"); // "apple" (T = string)
longest([1, 2, 3], [4]); // number[]
longest(10, 20); // Error: number has no 'length'The return type is still T, not { length: number }, so longest("a", "b").toUpperCase() works. A constraint limits what may come in without discarding the precise type.
A constraint can reference another type parameter. The classic case is a key that must belong to an object:
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const cfg = { host: "localhost", port: 8080 };
getProp(cfg, "port"); // number
getProp(cfg, "user"); // Error: '"user"' is not assignable to '"host" | "port"'Other useful shapes:
T extends string | number restricts to primitives usable as keys.T extends (...args: any[]) => any accepts any function, after which ReturnType<T> and Parameters<T> become available.T extends new (...args: any[]) => any accepts any class constructor.T extends readonly unknown[] accepts arrays and tuples without allowing mutation.Just like default function parameters, a type parameter can have a default that applies when the caller neither passes a type argument nor gives the compiler enough to infer one:
class EventEmitter<TEvents extends Record<string, unknown[]> = Record<string, unknown[]>> {
private listeners: { [K in keyof TEvents]?: ((...args: TEvents[K]) => void)[] } = {};
on<K extends keyof TEvents>(event: K, fn: (...args: TEvents[K]) => void) {
(this.listeners[event] ??= []).push(fn);
}
}
const generic = new EventEmitter(); // uses the default
const typed = new EventEmitter<{ login: [userId: string] }>();
typed.on("login", (id) => id.toUpperCase()); // id: stringRules: defaults must satisfy their own constraint, required type parameters cannot follow defaulted ones, and a default applies only when inference has no other source.
Explicit type arguments. When inference picks something too wide, pass the argument yourself: useState<string | null>(null).
const type parameters (TypeScript 5.0). Normally T inferred from an array literal is widened to string[]. Adding const before the parameter infers the readonly literal tuple instead, as if the caller had written as const:
function routes<const T extends readonly string[]>(paths: T): T { return paths; }
const r = routes(["/home", "/about"]); // readonly ["/home", "/about"]NoInfer<T> (TypeScript 5.4). Marks a position that should not contribute to inference, so one argument decides T and the others are checked against it:
function createRange<T extends string>(values: T[], initial: NoInfer<T>) { /* ... */ }
createRange(["draft", "live"], "live"); // OK
createRange(["draft", "live"], "archived"); // Error: not assignable to "draft" | "live"Without NoInfer, T would silently widen to include "archived".
| Message | Meaning |
|---|---|
| Type 'X' does not satisfy the constraint 'Y' | The type argument (explicit or inferred) breaks extends Y |
| Property 'p' does not exist on type 'T' | Add a constraint that includes p |
| 'T' could be instantiated with an arbitrary type | You returned a specific type where the caller's T was expected |
any or object when a precise shape ({ id: string }) would document the requirement better.<T extends string> when the function never uses string behaviour; drop the unnecessary constraint..tsx files, <T>(x: T) => x is parsed as JSX; write <T,>(x: T) => x or use a function declaration.Why does `function f<T>(x: T) { return x.length; }` fail to compile?
T extends Shape tells the compiler the minimum structure of every argument while keeping the precise return type.K extends keyof T is the standard way to type property access.T = string) apply only when nothing is passed or inferred and must satisfy their own constraint.const type parameters infer readonly literal types; NoInfer<T> stops a position from widening inference.Next lesson: keyof, typeof and Indexed Access Types — derive types from existing types and values instead of repeating them.