TypeScript

Generic constraints and defaults

A bare type parameter can be anything, which means you can safely do almost nothing with it. Constraints narrow "anything" down to "anything with at least these properties" — precise enough to actually use, still general enough to stay generic.

Advanced

4 min read

The problem, restated concretely from the previous lesson

function getLength<T>(item: T): number {
  return item.length; // Error: Property 'length' does not exist on type 'T'
}

T could be bound to literally anything — a number, a plain object with no length field, anything — so TypeScript refuses to let the function body assume item has a .length property at all. This is the exact same trade-off the previous lesson's double<T> example ran into: an unconstrained T is maximally general, and maximally general means you can safely do almost nothing with a value typed only as T.

extends: constraining a type parameter to "at least this shape"

interface HasLength {
  length: number;
}
 
function getLength<T extends HasLength>(item: T): number {
  return item.length; // fine — T is guaranteed to have a .length property now
}
 
getLength("hello");        // fine — strings have .length
getLength([1, 2, 3]);       // fine — arrays have .length
getLength({ length: 10 });   // fine — matches HasLength structurally
getLength(42);              // Error: number doesn't satisfy HasLength

<T extends HasLength> narrows T from "could be absolutely anything" to "could be anything, as long as it has at least a numeric length property" — the exact same structural-typing rule from earlier in this domain applies here too: T doesn't need to be a HasLength by name, it just needs the right shape. This is the standard way to give a generic function enough real information to actually do something useful with its parameter, without sacrificing genericity entirely by hardcoding one specific concrete type.

Constraining to keyof another type parameter — the pattern behind a safe, generic property getter

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
 
const person = { name: "Ada", age: 36 };
getProperty(person, "name"); // fine, returns string
getProperty(person, "age");   // fine, returns number
getProperty(person, "email");  // Error: Argument of type '"email"' is not assignable
                                 //        to parameter of type '"name" | "age"'

keyof T produces a union of T's own property-name literal types — for person, that's "name" | "age". Constraining a second type parameter K to keyof T means key can only ever be a property name that genuinely exists on obj, checked at compile time — and the return type T[K] (an indexed access type) is exactly the type of that specific property, not a vague unknown. This single pattern is what lets a genuinely generic "get a property safely" function exist at all, with the compiler catching a typo'd property name the same way it would catch one written directly as person.emial.

Generic defaults: a fallback type parameter when none is specified

interface ApiResponse<T = unknown> {
  status: number;
  data: T;
}
 
const response: ApiResponse = { status: 200, data: "anything at all, unknown here" };
const typedResponse: ApiResponse<{ id: number }> = { status: 200, data: { id: 1 } };

T = unknown gives the type parameter a default, used whenever the generic is referenced without explicitly supplying one — ApiResponse (with no <...> at all) is really ApiResponse<unknown>. This is the generics equivalent of a default function parameter value: it makes the common, unspecified case still type-check reasonably (falling back to unknown rather than erroring out for missing a type argument), while still allowing full precision (ApiResponse<{ id: number }>) whenever it's actually needed.

Constraints and defaults composed together

interface Repository<T extends { id: string | number }, K = T["id"]> {
  findById(id: K): T | undefined;
  save(item: T): void;
}

T extends { id: string | number } requires every entity type used with Repository to at least have an id field of a sensible type. K = T["id"] then defaults the key-lookup type to whatever T's own id type actually is — so Repository<User> automatically infers the right ID type from User itself, without the caller having to separately spell out Repository<User, number> by hand. Constraints keep a generic honest about what it actually needs; defaults keep the common case ergonomic without losing precision when it matters.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Why does `function getLength<T>(item: T): number { return item.length; }` fail to compile without a constraint?

2. What does `<T extends HasLength>` actually mean for what T can be?

3. In `function getProperty<T, K extends keyof T>(obj: T, key: K): T[K]`, what does constraining K to `keyof T` accomplish?