Skip to content

Generics and constraints

A generic carries the caller's type from input to output; a constraint says what it must have; keyof and T[K] tie keys to values.

Predict first

Before you read: predict

ExamHall's admin screens show the newest row of a list. Three helpers were written for it, and this file is compiled with tsc, strict: true:

On which lines does tsc report an error?

Code for this question
interface Attempt { id: string; score: number }interface Incident { id: string; severity: "low" | "high" } function newestAny(rows: any[]): any { return rows[rows.length - 1]; }function newestOf(rows: (Attempt | Incident)[]): Attempt | Incident { return rows[rows.length - 1]; }function newest<T>(rows: T[]): T { return rows[rows.length - 1]; } declare const attempts: Attempt[]; newestAny(attempts).scroe;   // line 10newestOf(attempts).score;    // line 11newest(attempts).score;      // line 12newest(attempts).scroe;      // line 13
Pick one

The 30-second version

A type parameter is a blank each call fills: newest<T>(rows: T[]): T hands back what it was given, where any or a union forgets it. extends sets a minimum the body may use and every argument must meet. keyof T is T's keys; T[K] is the type at key K. A parameter used only once relates nothing.

A generic carries the caller's type from input to output; a constraint says what it must have; keyof and T[K] tie keys to values.

How it works

Step 1 of 12

A type parameter is a blank the call fills. In newest<T>(rows: T[]): T, T is the caller's element type, and the return names the same T.
In the kitchen
  • the armchair from the van goes back as that armchair, re-covered → the return type names the caller's T (step 1)

Analogy: An upholsterer re-covering customers' own furniture (soft furnishings)

A van brings chairs and sofas in for new covers; each goes back to its owner.

At the upholsterer's In TypeScript
the armchair from the van goes back as that armchair, re-covered the return type names the caller's T (step 1)
the foreman reads the piece off the van; nobody tells him what it is inference (step 2)
a shop that hands back "a piece of furniture", whatever came in an any or union return (step 4)
he takes only pieces with a wooden frame to tack into extends (steps 5–6)
the order sheet's "re-cover" box lists only the parts this piece has K extends keyof T (steps 8–9)
the fabric cut for "seat cushion" fits that cushion T[K] (step 10)
a sheet reading "some part" gets fabric for any part a keyof T parameter (step 11)

In real terms: the compiler substitutes the call's argument types for the type parameters, checks them against each constraint, and computes the return type from the same substitution.

Breaks down when: the foreman inspects the piece on the day. tsc checks before anything runs, and the emitted JavaScript has no type parameters at all (ts-runtime-boundary-validation).

Contrasting case: a furniture shop selling new sofas. What you get does not depend on what you brought, so nothing needs carrying through (step 12).

Examples, run for real

4.1 ExamHall EXH-3370: a renamed field and a blank column

ExamHall's admin table renders attempts, candidates and incidents from one renderRows<Row>(rows, columns). A release renamed Candidate.email to contactEmail, and the candidates table showed an empty Email column (EXH-3370): its columns were { key: string }. MR !3670 typed the key as keyof Row:

interface Column<Row> { key: string; header: string }        // before MR !3670
interface Column<Row> { key: keyof Row; header: string }     // MR !3670

const candidateColumns: Column<Candidate>[] = [
  { key: "name", header: "Name" },
  { key: "email", header: "Email" },
];
EXH-3370 key: string    tsc: no diagnostics · emitted JS renders c-41 as ["Ada Lovelace", ""]
MR !3670 key: keyof Row tsc: TS2322 at the { key: "email" } line · the attempts columns compile unchanged

With key: string, any text was a key, so the stale column compiled and printed an empty cell. With keyof Row, Column<Candidate> accepts only Candidate's own keys, so the rename broke the build instead of the page (steps 8–9).

4.2 Ticketing: one key, one value type

function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const seats = get(order, "seats");   // order: { ref: string; seats: number; paid: boolean }
const paid = get(order, "paid");
get(order, "total");
seats: number · paid: boolean · get(order, "total"): TS2345

T is inferred as Order and K as the literal key, so each result has that property's type (steps 2, 10); "total" is not in keyof Order (step 9).

Picture it

Concept illustration for this lesson.
An illustration to help it stick. The mechanism is in the sections above.

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

A box office ranks seat offers by price. The file is compiled with strict: true:

On which lines does tsc report an error?

Code for this question
interface Priced { price: number }interface SeatOffer { price: number; row: string; seat: number } function cheapestOf(offers: Priced[]): Priced { return offers.reduce((a, b) => (b.price < a.price ? b : a)); }function cheapest<T extends Priced>(offers: T[]): T { return offers.reduce((a, b) => (b.price < a.price ? b : a)); } declare const offers: SeatOffer[];const rowA = cheapestOf(offers).row;   // line 8const rowB = cheapest(offers).row;     // line 9cheapest([{ row: "C", seat: 4 }]);     // line 10
Pick one

Question 2 of 7

A payouts screen reads one field at a time. Compiled with strict: true:

What type does tsc give c?

Code for this question
interface Payout { id: string; amount: number; currency: "EUR" | "GBP" } function read(p: Payout, key: keyof Payout) {  return p[key];} declare const payout: Payout;const c = read(payout, "currency");
Pick one

Question 3 of 7

A chat app compares room ids, some of which arrive as numbers. Compiled with strict: true:

On which lines does tsc report an error?

Code for this question
function sameRoom<T>(a: T, b: T): boolean {  return a === b;} sameRoom("r-12", "r-13");                // line 5sameRoom("r-12", 12);                    // line 6sameRoom<string | number>("r-12", 12);   // line 7
Pick one

Question 4 of 7

A podcast app's utility file declares four generic functions, and calls each one three times on episodes (id, title, durationSec). A reviewer says one type parameter is overkill: deleting it, and writing unknown or its constraint where it was used, changes no error and no inferred type at any of those calls.

Which function's type parameter?

Pick one

Question 5 of 7

The interviewer says: "Our product grid keeps reading one field out of a list. Type a function pluck(items, key) so that pluck(products, "price") is number[], pluck(products, "sku") is string[], and a key that Product does not have is a build error." You have 10 minutes and a plain editor, and you say what you will check first.

Write the signature and the body of pluck.

Question 6 of 7

A chat app picks the room with the most unread messages. Its rooms are Room { id: string; name: string; unread: number }. Compiled with strict: true, tsc reports TS2339 twice on the return line:

The team wants three things: the body compiles, busiest(rooms).name compiles for rooms: Room[], and busiest([{ name: "general" }]) is reported.

Which declaration gives all three?

Code for this question
function busiest<T>(rooms: T[]): T {  return rooms.reduce((a, b) => (b.unread > a.unread ? b : a));}
Pick one

Question 7 of 7

A box office's bookings report lets staff switch one column between seats and references. Compiled with strict: true:

What type does tsc give column?

Code for this question
interface Booking { ref: string; seats: number; holds: string[] } function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {  return items.map((item) => item[key]);} declare const bookings: Booking[];declare const bySeats: boolean;const column = pluck(bookings, bySeats ? "seats" : "ref");
Pick one

Go deeper

Pro tips
  1. When you need a key as a string, constrain it with & string. K extends keyof T may also be a number or a symbol key, so key.toUpperCase() fails; K extends keyof T & string keeps the link and allows string methods. When: building labels, paths or messages from keys (steps 8–9). Verify (RAN, tips.mjs):
    tip 1 K extends keyof T: TS2339 at key.toUpperCase() · K extends keyof T & string: no diagnostics
  2. Test a generic signature with // @ts-expect-error. Put the comment above a call that must not compile; if a change makes it compile, tsc reports the unused comment (TS2578), so a loosened signature fails the build. When: changing a shared helper's type parameters. Verify (RAN, tips.mjs):
    tip 2 // @ts-expect-error above pluck(shows, "city"): K extends keyof T -> no diagnostics · key: string, any[] -> TS2578 (unused @ts-expect-error)
Source

Quotes are checked verbatim by quote-check.mjs. Every graded fact is executed, so no item is keyed on these pages; they back the teaching. Valid as of 2026-10-09.

  • TypeScript Handbook, Generics (the curriculum row's first source), https://www.typescriptlang.org/docs/handbook/2/generics.html (ts-generics.txt). Of an any identity: "we actually are losing the information about what that type was when the function returns" (step 4); the type variable lets us "traffic that type information in one side of the function and out the other" (step 1); "the compiler just looked at the value "myString", and set Type to its type" (step 2); "the compiler will enforce that you use any generically typed parameters in the body of the function correctly" (step 5); "As long as the type has this member, we’ll allow it, but it’s required to have at least this member." (step 6); "We’d like to ensure that we’re not accidentally grabbing a property that does not exist on the obj, so we’ll place a constraint between the two types" (step 9).
  • TypeScript Handbook, Keyof Type Operator (the row's second source), https://www.typescriptlang.org/docs/handbook/2/keyof-types.html (ts-keyof-types.txt): "The keyof operator takes an object type and produces a string or numeric literal union of its keys." (step 8).
  • TypeScript Handbook, Indexed Access Types (the row's third source), https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html (ts-indexed-access-types.txt): "We can use an indexed access type to look up a specific property on another type" and "The indexing type is itself a type, so we can use unions, keyof, or other types entirely" (steps 10–11).
  • TypeScript Handbook, More on Functions (not named by the row; teaching only, keys nothing), https://www.typescriptlang.org/docs/handbook/2/functions.html (ts-functions.txt): "Normally it would be an error to call this function with mismatched arrays" and "If you intended to do this, however, you could manually specify Type" (step 3); "the function promises to return the same kind of object as was passed in, not just some object matching the constraint" (step 7); "Rule: If a type parameter only appears in one location, strongly reconsider if you actually need it" (step 12).

Checked against the tools and docs as of 2026-10-09.