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?
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 13The 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
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

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?
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 10Question 2 of 7
A payouts screen reads one field at a time. Compiled with strict: true:
What type does tsc give c?
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");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?
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 7Question 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?
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?
function busiest<T>(rooms: T[]): T { return rooms.reduce((a, b) => (b.unread > a.unread ? b : a));}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?
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");Go deeper
Pro tips
- When you need a key as a string, constrain it with
& string.K extends keyof Tmay also be a number or a symbol key, sokey.toUpperCase()fails;K extends keyof T & stringkeeps 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 - Test a generic signature with
// @ts-expect-error. Put the comment above a call that must not compile; if a change makes it compile,tscreports 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 ananyidentity: "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).
Related lessons
Checked against the tools and docs as of 2026-10-09.