skip to content

Type Parameter Constraints

Narrowing what a type parameter is allowed to be so the compiler lets you actually use it — and so callers cannot pass nonsense. Constraints are also the main tool for tying two type parameters together, which is where most real library typings live.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

10

In TypeScript, `function longest<T>(a: T, b: T) { return a.length >= b.length ? a : b; }` does not compile. Why does the checker reject `a.length`, and what does changing the signature to `<T extends { length: number }>` change?

level: juniorimportance: must knowfreq 76%

answer

  1. the caller picks T, not the function
  2. body must be valid for every instantiation
  3. read extends as "assignable to"
  4. constraint is a floor, not the parameter type
  5. structural: strings, arrays and functions qualify

basics

~20 s

An unconstrained T has no known members, so a.length is an error: "Property 'length' does not exist on type 'T'". Adding <T extends { length: number }> promises every T the caller picks has a numeric length, so the body may read it.

solid answer

~50 s

A bare type parameter is a placeholder the *caller* fills in, and the body has to type-check once for every possible instantiation. Since the caller could pass a `number`, the checker admits only what every type guarantees — which is nothing — so `a.length` fails with `Property 'length' does not exist on type 'T'`. `T extends { length: number }` is a constraint, and it is a two-way bargain: it restricts which types a caller may pass to those assignable to `{ length: number }`, and in exchange the body may treat `a` as having a numeric `length`. The check is structural, not inheritance, so strings, arrays, tuples, functions and any object literal with a numeric `length` all qualify, while a `Map` (which has `size`) is rejected at the call site. Inference still picks the argument's own type for `T`, so a caller passing `string[]` gets `string[]` back.

code

typescript · 20 lines
typescript
// Unconstrained: the body knows nothing about T.
// function longest<T>(a: T, b: T) {
//   return a.length >= b.length ? a : b;
//   Error TS2339: Property 'length' does not exist on type 'T'.
// }

// Constrained: callers narrow, the body widens.
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

const word: string = longest("alpha", "be");
const nums: number[] = longest([1, 2], [1, 2, 3]);
const box: { length: number; tag: string } = longest(
  { length: 3, tag: "a" },
  { length: 9, tag: "b" },
);

// longest(new Map(), new Map());
// Error TS2345: Property 'length' is missing in type 'Map<any, any>'.

go deeper

for a junior

Be able to say that a bare T guarantees nothing, so no property access is allowed, and that extends adds the shape the body needs. Write the fixed signature on the spot.

for a middle

Explain the bargain in both directions — call sites narrow, the body widens — and that assignability is structural, so a Map with size does not satisfy a length constraint.

for a senior

Show you read the error direction: a missing-property error inside the body means the constraint is too loose, while a call-site assignability error means it is too tight. Mention that the guarantee dies at any boundary.

for a principal

Own the framing that a constraint is published API: it is what you promise callers you will accept, so constrain to the members you actually read and no more, and treat tightening it later as a breaking change.

## Why an unconstrained type parameter is opaque A generic function is checked **once**, not once per call. When you write `<T>`, `T` is a placeholder that the caller — not the function — chooses. The compiler must therefore prove the body is safe for *every* type the caller could pick: `string`, `number`, `null`, a class instance, a union. The only members it can safely admit are the ones every one of those types has, and that set is empty. An unconstrained type parameter behaves as if it were constrained to `unknown`. So this is rejected: ```ts function longest<T>(a: T, b: T) { return a.length >= b.length ? a : b; // ~~~~~~ Property 'length' does not exist on type 'T'. } ``` The error is not that `length` is missing from the *value* you happen to pass — it is that the signature never promised it. ## What `extends` means in this position `extends` in a type-parameter list is **not** class inheritance. Read it as *"is assignable to"*: `T extends C` means "whatever the caller substitutes for `T` must be assignable to `C`". TypeScript's assignability is structural, so the constraint describes a *shape*, not an ancestor. The constraint is a bargain with two halves: - **Call sites lose freedom.** Arguments that are not assignable to the constraint are rejected there, with an error like `Argument of type 'Map<any, any>' is not assignable to parameter of type '{ length: number; }'. Property 'length' is missing in type 'Map<any, any>'`. - **The body gains knowledge.** Inside the function, every value of type `T` may be treated as having the constrained members. ```ts function longest<T extends { length: number }>(a: T, b: T): T { return a.length >= b.length ? a : b; } longest("alpha", "be"); // ok — string has length longest([1, 2], [1, 2, 3]); // ok — arrays have length longest({ length: 3 }, { length: 9 }); // ok — structural ``` ## Which types satisfy `{ length: number }` Because the check is structural, the qualifying set is wider than people expect: `string`, every array and tuple type, functions (a function's `length` is its arity), and any object with a numeric `length` property — including one that carries extra properties, since `T` is inferred as the whole argument type. `Map` and `Set` are rejected: they expose `size`, and a structural check does not care that the *idea* is similar. ## The constraint is a floor, not the type of the value A very common misreading is that `T extends { length: number }` turns the parameter into `{ length: number }`. It does not. The constraint is the minimum the body may rely on; inference still resolves `T` to the argument's real type, so the caller's specificity survives the call when you return `T`: ```ts declare function longest<T extends { length: number }>(a: T, b: T): T; const s = longest("alpha", "be"); // s: string ``` ## The escape hatches, and why they are worse Two shortcuts remove the error without the guarantee. Typing the parameters as `any` silences every member check, including the typos; and asserting `(a as { length: number }).length` is an unchecked claim the compiler simply trusts. The constraint gives the same body access while keeping call sites honest. ## Constraints are erased Nothing about this survives compilation. The emitted JavaScript is the function body with the annotations stripped — there is no runtime test that the argument has a `length`. If a value reaches the function through `any` (a `JSON.parse` result, an untyped third-party call), the compile-time promise was never checked and the property access can be `undefined` at runtime. Validating untrusted data at the boundary is a separate job the type layer does not do for you. ## Getting the error message right When the constraint is wrong you get one of two very different errors, and knowing which is which speeds up debugging: a `Property 'x' does not exist on type 'T'` error inside the body means the **constraint is too loose** for what you are doing; an `Argument of type '…' is not assignable to parameter of type '…'` error at a call site means the **constraint is too tight** for what your callers have.

  • Does adding the constraint change what type the caller gets back?
    No. The constraint only limits what may be passed; inference still resolves `T` to the argument's own type. `longest("a", "bb")` with a `: T` return type still yields `string`, not `{ length: number }`. You lose the caller's type only if you *declare* the return type as the constraint.
  • Why is `Map` rejected by a `{ length: number }` constraint even though it clearly has a size?
    Assignability in TypeScript is structural on names and types, not on intent. `Map` exposes `size`, not `length`, so it is not assignable to `{ length: number }` and the call site errors with `Property 'length' is missing in type 'Map<any, any>'`. Constrain on the member you actually read.
  • What is the runtime cost of a type-parameter constraint?
    None. Type parameters and their constraints are erased during compilation, so the emitted JavaScript contains no check that the argument has a `length`. The guarantee holds only for values the compiler actually saw; anything arriving through `any` bypasses it entirely.

saying these in an interview costs you the question

  • Thinks extends here means class inheritance
  • Says the parameter becomes the constraint type inside the function
  • Believes the constraint is checked at runtime
  • Reaches for any instead of a constraint to silence the error
  • Confuses <T extends X> with the default <T = X>

context

open as a page

In TypeScript, given `declare function get<T, K extends keyof T>(obj: T, key: K): T[K]`, what type does `get({ id: 1, name: 'ada' }, 'name')` produce, and what is the `K extends keyof T` constraint doing?

level: juniorimportance: must knowfreq 68%

basics

~20 s

It produces string. keyof T is the union of T's property names, so K is constrained to those names and infers the literal 'name' at this call; the return type T[K] then resolves to that one property's type.

open as a page

In TypeScript, a helper declared `function track<T extends { id: string }>(x: T): T` could instead declare its return type as the constraint, `{ id: string }`. What difference does that make to callers, and which should you prefer?

level: middleimportance: must knowfreq 58%

basics

~20 s

Returning T preserves the caller's exact type, so extra properties survive the call. Returning the constraint widens every result to { id: string } and silently discards the rest. Prefer T — carrying the caller's type through is the reason the generic exists.

open as a page

In TypeScript, why write `function get<T, K extends keyof T>(obj: T, key: K): T[K]` instead of the simpler `function get<T>(obj: T, key: keyof T): T[keyof T]`?

level: middleimportance: must knowfreq 58%

basics

~20 s

Giving the key its own type parameter captures which key was passed. With key: keyof T the compiler only knows it was some key, so the return type T[keyof T] is the union of every property type, and the caller must narrow it back down.

open as a page

In TypeScript generics, what does each of the constraints `T extends object`, `T extends {}` and `T extends unknown` actually allow a caller to pass?

level: middleimportance: should knowfreq 40%

basics

~20 s

object means non-primitive, so arrays, functions and class instances pass while strings and numbers are rejected. {} means anything except null and undefined, so primitives pass. unknown constrains nothing at all — it is the same set as an unconstrained parameter.

open as a page

In TypeScript, why does `obj[key] = obj[key] + 1` fail to compile inside `function bump<T, K extends keyof T>(obj: T, key: K)`, and how do you type a helper that only accepts keys whose property is a number?

level: middleimportance: should knowfreq 42%

basics

~20 s

Inside the function T is still unresolved, so T[K] is a deferred type the checker cannot prove is number, and arithmetic on it is rejected. Fix it by relating the parameters the other way: constrain the object to a Record whose value at the key is number.

open as a page

In TypeScript, `function make<T extends { name: string }>(): T { return { name: "anon" }; }` fails with "'{ name: string; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ name: string; }'". What is the compiler protecting against, and how do you fix it?

level: seniorimportance: should knowfreq 45%

basics

~20 s

The caller chooses T, not the function, and may choose a subtype requiring more than name. A value that merely satisfies the constraint is therefore not a valid T. Fix the signature — return the concrete type or derive the value from a T the caller passed in.

open as a page

In TypeScript, `declare function set<T, K extends keyof T>(o: T, k: K, v: T[K]): void`. If `o` is `{ a: 'x', b: 1 }` and `k` is a variable declared as `'a' | 'b'`, does `set(o, k, 1)` compile, and is it safe?

level: seniorimportance: should knowfreq 30%

basics

~20 s

It compiles but is unsafe. K infers as the union 'a' | 'b', so T[K] widens to string | number and the number 1 is accepted — even though at runtime k may be 'a', writing a number into a property typed string.

open as a page

You are designing a shared TypeScript utility library. How do you decide how tight a generic constraint should be, and what goes wrong when a constraint is tighter than the function body actually needs?

level: principalimportance: should knowfreq 34%

basics

~20 s

Constrain to the members the body actually reads, and no more. An over-tight constraint rejects callers whose values would work perfectly at runtime, pushing them into as assertions that relocate unsoundness into their code — and loosening it later changes your published contract.

open as a page

In TypeScript, given `interface Comparable<U> { compareTo(other: U): number }`, what does the self-referential constraint in `function max<T extends Comparable<T>>(a: T, b: T): T` express, and when do you need it rather than `T extends Comparable<unknown>`?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

It requires T to be comparable against itself: any type used here must declare compareTo taking that same type. That is what makes max(a, b) meaningful, since both arguments are T and each must accept the other.

open as a page