Kodokon kodokon.com

Advanced generics: constraints and defaults

You constrain your type parameters with extends and provide defaults to build ergonomic generic APIs.

10 min · 3 questions

Open this lesson in Kodokon

An unconstrained generic is opaque: inside the function body, T lets you do nothing. The extends clause establishes a minimal contract on the type parameter, which unlocks access to the guaranteed properties while preserving the caller's precise type. That is the fundamental difference from a parameter typed with an interface: the constraint keeps the information, the interface erases it.

TYPESCRIPT
function longest<T extends { length: number }>(
  a: T,
  b: T
): T {
  return a.length >= b.length ? a : b;
}

const word = longest("hello", "worldwide");
const list = longest([1, 2], [3, 4, 5]);
word stays typed as string, list as number[].

The K extends keyof T duo is the most profitable everyday pattern: it links two type parameters together and statically guarantees that a key actually belongs to the object. The T[K] return type (an indexed access type) precisely tracks the requested property. This is exactly how functions like pick or pluck from utility libraries are typed.

TYPESCRIPT
function getProp<T extends object, K extends keyof T>(
  obj: T,
  key: K
): T[K] {
  return obj[key];
}

const config = { port: 8080, host: "localhost" };
const port = getProp(config, "port");
const host = getProp(config, "host");
port is inferred as number, host as string; any other key is rejected.

Defaults (T = string) make a generic optional at the call site: users only provide the type argument when they want to deviate from the common case. Combined with a constraint (T extends X = Y), they define both the bound and the implicit behavior. The default must satisfy the constraint, otherwise the declaration itself does not compile. This is the standard pattern for HTTP clients, stores, and typed event emitters.

TYPESCRIPT
interface ApiResponse<T = unknown> {
  status: number;
  data: T;
}

async function fetchJson<T = unknown>(
  url: string
): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  return { status: res.status, data: await res.json() };
}

interface Post { id: number; title: string }
const typed = await fetchJson<Post>("/api/posts/1");
const loose = await fetchJson("/api/health");
Without a type argument, data falls back to unknown, not any.

Knowledge check

Make sure you remember the key points of this lesson.

  1. What does the constraint K extends keyof T guarantee in a generic signature?
    • That K is any string whatsoever
    • That K is an existing key of T, checked at compile time
    • That T contains at least one optional key
    • That K and T are the same type
  2. What advantage does longest<T extends { length: number }> have over longest(a: { length: number }, b: ...) without generics?
    • It runs faster
    • It preserves the caller's precise type in the return value
    • It accepts more input types
  3. Why prefer <T = unknown> over <T = any> as a default value?
    • unknown compiles faster than any
    • any is forbidden in strict mode
    • unknown forces the caller to narrow the type before use, any disables all checking
    • unknown accepts fewer values than any