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
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”.