TypeScript path · Modelling data · Topic 6 of 11

Generics and constraints

Not started

A generic function has a type parameter, written <T>, that stands for whatever type the caller uses. Its job is to connect types: the item type of an array to the type of the result, the type of a key to the type of the value it looks up. Callers almost never write the type argument themselves; it is inferred from what they pass. And because types are erased, generics cost nothing at runtime: first<T> compiles to the same function as first.

Why it matters: Without generics, a helper that works for any type has to return `any` or `unknown` and lose what the caller knew. With them, the type flows through.

  1. 1A type parameter links things

    `first<T>(xs: T[]): T | undefined` says the result has the same type as the array's items. The caller rarely writes `T`: it is inferred from the arguments.

  2. 2Constraints say what T must have

    `T extends { id: string }` lets the function read `.id` while still returning the caller's full type, not just `{ id: string }`.

  3. 3Use them only when they relate two things

    If a type parameter appears only once, it relates nothing and a plain type does the same job. Fewer type parameters give clearer errors.

1

How it works

The explanation, with examples you can run.

Linking input and output

function first<T>(xs: T[]): T | undefined {  return xs[0]} const a = first([1, 2, 3]) // number | undefinedconst b = first(["x"]) // string | undefinedconst c = first<boolean>([]) // written out when there is nothing to infer from

Compare function first(xs: unknown[]): unknown. It is just as correct, but every caller has to check or cast the result again, because the function threw away what the caller knew.

Constraints

Inside a generic function, T could be anything, so you can't use any of its members. A constraint narrows what callers may pass and, in exchange, tells the body what it can rely on:

function byId<T extends { id: string }>(items: T[], id: string): T | undefined {  return items.find((item) => item.id === id)} type Product = { id: string; title: string; price: number }declare const products: Product[] const p = byId(products, "p1") // Product | undefined, with title and pricebyId([1, 2, 3], "1") // Error: number does not have an id

The common partner of constraints is keyof, which ties a key to the object it belongs to:

function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {  return obj[key]} const user = { name: "Ada", age: 36 }const age = pluck(user, "age") // numberpluck(user, "email") // Error: "email" is not a key of the user

T[K] is an indexed access type: the type of property K of T. Here two type parameters genuinely relate: K depends on T, and the result depends on both.

Generic types

Types take parameters too, which is how Array<T>, Promise<T> and Map<K, V> work:

type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E } function parsePort(raw: string): Result<number, string> {  const n = Number(raw)  return Number.isInteger(n) ? { ok: true, value: n } : { ok: false, error: `not a port: ${raw}` }}

E = Error is a default, used when the caller leaves the second argument out.

Restraint

The handbook gives three rules worth keeping: use the type parameter itself rather than constraining it more than needed, use as few type parameters as possible, and if a type parameter appears in only one place, you probably don't need it. function log<T>(value: T): void relates nothing: value: unknown says the same. Generics are for connecting types; when nothing needs connecting, a plain type gives simpler signatures and clearer error messages.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.What is the type of `x`?
    function first<T>(xs: T[]): T | undefined {  return xs[0]}const x = first(["a", "b"])
  2. 2.Why write `function byId<T extends { id: string }>(items: T[], id: string): T | undefined` instead of taking `{ id: string }[]`?
  3. 3.What is wrong with `function log<T>(value: T): void`?

0 of 3 right so far. Get all three to finish this topic.