skip to content

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