codejump

AcademyTypeScript

Generics: a type the caller fills in

What a type parameter actually does, why extends means constrain rather than inherit, and the rule that tells you when a generic is earning its keep.

3 min readUpdated

3 exercises

Waiting at the end of this page.

The relationship you could not otherwise state

Without generics, a function that returns what it was given has to lie:

TypeScript
function first(items: any[]): any { return items[0]; }

const name = first(['a', 'b']);  // any. The compiler has stopped helping.

A type parameter lets you say this comes back as whatever went in:

TypeScript
function first<T>(items: T[]): T | undefined { return items[0]; }

first(['a', 'b']);  // string | undefined
first([1, 2]);      // number | undefined

T is not a type. It is a hole the caller fills, usually without noticing — TypeScript infers it from the argument. You only pass one explicitly when there is nothing to infer from.

extends means "at least this", not "inherits from"

The keyword is reused and it misleads everyone once. In a type parameter, extends is a constraint: it restricts what the caller may pass.

TypeScript
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest('abc', 'de');        // string
longest([1, 2], [3]);        // number[]
longest(1, 2);               // Error: number has no 'length'

Without the constraint, a.length would not compile — inside the function, an unconstrained T could be anything, so you can do nothing with it. That is the trade generics make: the more you constrain, the more you can do in the body, and the fewer callers you serve.

Two mistakes worth recognising

A type parameter used once. If T appears in only one position, it is doing nothing a plain type could not:

TypeScript
function log<T>(value: T): void { console.log(value); }
// T is used once. This is just (value: unknown) => void.

A generic earns its place by relating two things: an argument to a return value, one argument to another, a key to the type of that key.

Returning a type the caller chose. This compiles and is almost always wrong:

TypeScript
function parse<T>(json: string): T {
  return JSON.parse(json);  // T is whatever the caller claimed
}

const user = parse<User>(raw);  // no checking happened anywhere

That is as with extra ceremony. Return unknown and make the caller narrow, or validate and return a real type.

Defaults, and inference you can steer

TypeScript
interface Paged<T, Meta = { total: number }> {
  items: T[];
  meta: Meta;
}

type Users = Paged<User>;  // Meta falls back to { total: number }

And when you want a literal type instead of the widened one, constrain to the primitive:

TypeScript
function pick<T extends string>(value: T): T { return value; }

pick('idle');  // 'idle' — not string

The constraint is what keeps the literal: T extends string infers as narrowly as it can, where a bare T widens.

You wantWrite
Whatever came in, unchanged<T>(x: T) => T
Only things with a property<T extends { id: string }>
A key of an object you were given<T, K extends keyof T>
A literal, not the widened type<T extends string>
A fallback the caller may override<T = string>

The rule

Reach for a generic when two types in a signature must agree, not when you want a function to accept several shapes — a union does that more simply and reads better in the error message. A good generic is invisible at the call site: the caller passes an argument and gets back a precise type without ever writing an angle bracket.

Now practice it

2 challenges · 1 quiz

Reading this page is the cheap half. These are the exercises that make you use it.