Generic Constraints (extends)

intermediate generics constraints extends

In simple language, a bare <T> accepts literally any type — which sometimes means we can’t do anything useful inside the function. Generic constraints (T extends Something) say “T can be anything, but at minimum it has these properties”.

// bare generic — we don't know T has .length
function logLength<T>(x: T) {
  console.log(x.length); // Error — Property 'length' does not exist on type 'T'
}

// constrained — T must have .length
function logLength2<T extends { length: number }>(x: T) {
  console.log(x.length); // OK
}

logLength2("hello");       // OK — strings have length
logLength2([1, 2, 3]);     // OK — arrays have length
logLength2({ length: 5 }); // OK — duck-typed
logLength2(42);            // Error — number has no length

How to think about constraints

flexibility vs. capability tradeoff
<T>
accepts: any type
can do: nothing T-specific
<T extends X>
accepts: anything matching X
can do: anything X allows
specific type
accepts: only that type
can do: everything

Constraint referring to another type parameter

We can have one type parameter constrain another. Very common when we want a “key of an object” type-safe getter.

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

const user = { name: "Manish", age: 28 };
const n = getProp(user, "name"); // n: string
const a = getProp(user, "age");  // a: number
getProp(user, "xyz");            // Error — "xyz" is not a key of user

This is one of the most-asked patterns in interviews — typed property access.

Constraints with default types

We can give a type parameter a default, used when the caller doesn’t specify one.

function createState<T = string>(initial: T): { value: T } {
  return { value: initial };
}

const a = createState("hi");        // T inferred as string
const b = createState<number>(0);   // T explicit
const c = createState();            // Error — initial required, but T defaults to string

Constraining to unions

We can require T to be one of a set of literal types.

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

function request<M extends HttpMethod>(method: M, url: string) {
  return { method, url };
}

request("GET", "/users");  // OK
request("PATCH", "/x");    // Error — not in the union

extends in conditionals — quick mention

extends also appears in conditional types (T extends U ? X : Y). Same keyword, different role — there it’s a type-level “is T assignable to U” check, not a constraint. We’ll dig into that in the conditional types note.

Common interview question

“Implement a type-safe pick(obj, ...keys).” The answer uses K extends keyof T and a rest tuple:

function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
  const out = {} as Pick<T, K>;
  for (const k of keys) out[k] = obj[k];
  return out;
}

const u = { id: 1, name: "M", age: 28 };
const p = pick(u, "id", "name"); // p: { id: number; name: string }

Constraints are what take generics from “neat” to “actually useful in production”.