skip to content

Generics

Parametric polymorphism in TypeScript: writing functions, types, and classes that work over a type parameter instead of one concrete type, and getting the compiler to infer that parameter for you. Interviewers lean on generics because they separate people who can read a library's types from people who can design them.

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

explore

questions

page 1 of 2

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, what does the `= unknown` in a declaration like `interface ApiResult<T = unknown>` do, and when does the compiler actually apply that default?

level: juniorimportance: must knowfreq 55%

basics

~20 s

A type-parameter default supplies the type argument when the caller omits it, so writing ApiResult with no argument means ApiResult<unknown>. TypeScript also falls back to the default in a generic call when inference finds no candidate for that parameter.

open as a page

In TypeScript, what does declaring `function identity<T>(x: T): T` give you that `function identity(x: any): any` does not?

level: juniorimportance: must knowfreq 84%

basics

~20 s

A type parameter links the argument type to the return type, so the call site keeps the concrete type it passed in and stays type-checked. any turns checking off instead: the result is unchecked and any misuse of it silently compiles.

open as a page

Given the TypeScript declaration `interface ApiResponse<T> { data: T; status: number }`, what does the `<T>` declare, and what is the type of `res.data` when `res` is annotated `ApiResponse<User[]>`?

level: juniorimportance: must knowfreq 72%

basics

~10 s

The <T> declares a type parameter: a placeholder the user of the interface fills in. Writing ApiResponse<User[]> substitutes User[] for T everywhere in the declaration, so res.data has type User[] while status stays number.

open as a page

In TypeScript, given `declare function first<T>(items: T[]): T | undefined`, what is the type of `first([1, 2, 3])`, and where did the compiler get `T` from?

level: juniorimportance: must knowfreq 72%

basics

~20 s

first([1, 2, 3]) has type number | undefined. TypeScript matches the argument type number[] against the parameter type T[] at the call site, infers T as number, and substitutes that into the declared return 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, `[1, 2, 3].map(x => x * 2)` compiles with `x` typed as `number`, but extracting the same arrow into `const cb = (x) => x * 2` and calling `.map(cb)` fails to compile. Why?

level: middleimportance: must knowfreq 70%

basics

~20 s

A function expression written directly in an argument position is contextually typed: its parameter types come from the expected callback type. A standalone const has no such context, so its parameter is an implicit any error under strict.

open as a page

A TypeScript HTTP helper is declared as `declare function fetchJson<T>(url: string): Promise<T>`, so callers write `fetchJson<User>('/api/me')`. What does that type parameter actually guarantee, and how would you type the helper instead?

level: middleimportance: must knowfreq 70%

basics

~20 s

Nothing. T appears only in the return type, so there is no inference site and the caller simply picks it — an unchecked assertion, equivalent to as User. Return Promise<unknown>, or take a validator parameter so T is derived from real runtime code.

open as a page

In a .tsx file, how do you type the props of a reusable List component so that its renderItem callback receives the element type of the items array instead of any, and where does that type come from at each usage?

level: middleimportance: must knowfreq 62%

basics

~20 s

Declare the type parameter on the component function and thread it through the props: function List<T>(props: { items: T[]; renderItem: (item: T) => ReactNode }). TypeScript infers T from the items passed at each usage site.

open as a page

In TypeScript with strict enabled, given `class Dog extends Animal`, `interface Box<T> { value: T }` and `interface Sink<T> { send: (value: T) => void }` — for each of Box and Sink, which instantiation is assignable to the other, and what rule decides the direction?

level: middleimportance: must knowfreq 50%

basics

~10 s

Box<T> is covariant because T only appears in an output position, so Box<Dog> is assignable to Box<Animal>. Sink<T> is contravariant because T appears only as a parameter, so Sink<Animal> is assignable to Sink<Dog>.

open as a page

Under `strict`, TypeScript accepts `const h: { handle(e: Event): void } = { handle(e: MouseEvent) {} }` but rejects the same object when the target type is written `{ handle: (e: Event) => void }`. Why?

level: middleimportance: must knowfreq 52%

basics

~20 s

TypeScript compares parameters of members declared with method shorthand bivariantly, so the narrower MouseEvent parameter is accepted. The property form is a plain function type, and strictFunctionTypes, which strict enables, compares those parameters contravariantly, making the narrowing an error.

open as a page

Compare these two TypeScript signatures: `function getFirst<T>(arr: T[]): T` and `function getLength<T>(arr: T[]): number`. Why does the type parameter earn its place in only one of them, and how would you rewrite the other?

level: juniorimportance: should knowfreq 45%

basics

~20 s

A type parameter earns its place only when it links two positions in a signature. getFirst returns T, so it carries the element type back to the caller; getLength never uses T again, so it should just take unknown[].

open as a page

In a .tsx file, `const identity = <T>(value: T) => value;` fails to compile although the identical line is fine in a .ts file. Why, and what are two ways to write it so it parses?

level: juniorimportance: should knowfreq 45%

basics

~20 s

In .tsx files the parser reads the leading angle bracket as the start of a JSX element, not a type parameter list. Disambiguate with a trailing comma, <T,>, add a constraint such as <T extends unknown>, or use function syntax.

open as a page

In a TypeScript interface, a callback member can be written as `onDone(result: string): void` or as `onDone: (result: string) => void`. Do the two forms differ in the emitted JavaScript, and are they interchangeable?

level: juniorimportance: should knowfreq 38%

basics

~20 s

Neither form emits anything: interfaces are erased, so the JavaScript is identical. They are not fully interchangeable to the checker, which compares method-shorthand parameters loosely in both directions but compares a property function type's parameters strictly under strict mode.

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, what rules govern default type parameters — where a default may appear in the list, what type it may reference, and how it interacts with an `extends` constraint on the same parameter?

level: middleimportance: should knowfreq 38%

basics

~20 s

Defaults must come last: once a type parameter has one, every later parameter needs one too. A default may reference parameters declared before it but not after, and when the parameter is also constrained the default must itself satisfy that constraint.

open as a page

In TypeScript, given `declare function pair<K, V>(k: K, v: V): [K, V]`, can you call `pair<string>('a', 1)` to pin K and let V be inferred? What is the rule for how many type arguments a call may supply?

level: middleimportance: should knowfreq 48%

basics

~20 s

No. TypeScript has no partial type-argument inference: a call supplies every type parameter that has no default, or none at all. Omitted trailing parameters take their declared defaults rather than being inferred from the arguments.

open as a page

In TypeScript, what is the difference between `type Mapper = <T>(x: T) => T[]` and `type MapperOf<T> = (x: T) => T[]`?

level: middleimportance: should knowfreq 41%

basics

~20 s

Position decides who picks T. In the first, the type takes no arguments and describes a generic function whose T is chosen fresh at each call. In the second, the type itself takes an argument and describes an ordinary non-generic function.

open as a page

In a `.tsx` file, `const identity = <T>(x: T) => x;` fails to compile even though the identical line is fine in a `.ts` file. Why, and how do you write a generic arrow function there?

level: middleimportance: should knowfreq 52%

basics

~20 s

In .tsx files the parser reads <T> as the opening tag of a JSX element, not a type parameter list, so the line never parses as a generic arrow. Disambiguate with a trailing comma, <T,>, with a constraint, or by using a function declaration.

open as a page

In TypeScript, given `class Store<T> { private items: T[] = []; add(item: T): void { this.items.push(item); } }`, when is `T` decided, and what changes if the type parameter is moved onto `add` instead of onto the class?

level: middleimportance: should knowfreq 42%

basics

~20 s

A class type parameter is decided once, when the class is instantiated or annotated, and stays fixed for that instance type, so the field and the method share it. Moving it onto the method makes it fresh at every call, breaking that link to the stored items.

open as a page

Why does TypeScript reject a static member that references the class type parameter, as in `class Box<T> { static empty: T }`, and what do you write instead when you want a static factory for a generic class?

level: middleimportance: should knowfreq 40%

basics

~20 s

TypeScript reports that static members cannot reference class type parameters. Statics live on the single constructor object shared by every instantiation, so there is no one T for them. Give the static its own type parameter instead.

open as a page

What does the `const` modifier on a type parameter do — `declare function f<const T>(x: T): T` in TypeScript 5.0 and later — and when does it have no effect?

level: middleimportance: should knowfreq 40%

basics

~20 s

It makes inference treat the argument as if the caller had written a const assertion: object literals come back with readonly literal-typed properties, arrays as readonly tuples. It affects only expressions written at the call site, and nothing at runtime.

open as a page

In TypeScript, `declare function box<T>(x: T): { value: T }` called as `box("red")` produces `{ value: string }` rather than `{ value: "red" }`. Why does the literal widen, and how would you change the signature to keep it?

level: middleimportance: should knowfreq 50%

basics

~20 s

TypeScript infers the fresh literal type "red" and then widens it to string, because nothing in the signature asks for a literal. Constraining the type parameter with T extends string, or marking it const, preserves the literal.

open as a page

For `declare function pair<T>(x: T, y: T): T[]` in TypeScript, how does the compiler resolve `T` when the two arguments suggest different types — compare `pair(1, "x")` with `pair(animal, dog)` where `Dog extends Animal`?

level: middleimportance: should knowfreq 48%

basics

~20 s

TypeScript collects one candidate per inference site and keeps the candidate that is a supertype of the others, so pair(animal, dog) gives Animal[]. When no candidate covers the rest, pair(1, "x") is a compile error rather than a silent union.

open as a page

In TypeScript, for `declare function make<T>(): T`, what type does `T` get at `const x = make()`, and what changes if the signature is `declare function makeStr<T extends string>(): T`?

level: middleimportance: should knowfreq 42%

basics

~10 s

With no argument mentioning T, inference has nothing to work from: T falls back to its constraint when it has one. So make() gives unknown, while makeStr() with T extends string gives string.

open as a page

In TypeScript, when is `declare function setTheme<T extends 'light' | 'dark'>(theme: T): void` better written as `declare function setTheme(theme: 'light' | 'dark'): void`, and when does the generic version genuinely earn its keep?

level: middleimportance: should knowfreq 45%

basics

~20 s

In that exact shape the generic is pure noise — both signatures accept identical arguments, because T is used once. A constrained type parameter only earns its keep when another parameter or the return type is expressed in terms of T.

open as a page

In TypeScript, how do you type a fluent builder so the type of the object being built grows with each chained call, and what goes wrong if every chaining method returns the builder's own type unchanged?

level: middleimportance: should knowfreq 40%

basics

~20 s

Make the builder generic in what it has accumulated so far and have each chaining method return a new instantiation, such as Builder<T & Record<K, V>>, rather than Builder<T>. Returning the same type throws away whatever the call just added.

open as a page

showing 1–30 of 52