Generics and utility types
Name a placeholder once. Reshape object types with Partial, Pick, Omit, and Record.
<!-- hal:authoritative:yaml -->
Name a placeholder once. Reuse it across parameters and return types. Then reshape object types with Partial, Pick, Omit, and Record instead of hand-copying fields.
§I — Frame
Duha Primary TypeScript session 5. A teammate’s 2026-09-18 kick seats Topics #9 here and defers Bun-centric rows 5–8 (Bun.serve, Bun.file, bun test, @types/bun) at this clock. Prior Asr sessions taught runtime, strict basics, install, and moduleResolution: bundler. Today the track turns to type reuse: generics and the built-in utilities that reshape object types.
Done-criteria: Can write a generic function/type, constrain with extends, and apply Partial / Pick / Omit / Record with intent.
Primary cites: Cherny Programming TypeScript Chapter 4 (generics under Polymorphism) and Chapter 6 (Record, mapped types) on disk at , plus the live TypeScript Handbook Utility Types for the catalog Cherny only partly freezes in 2019 (Omit lands later). Handbook Generics is secondary wording for constraints.
Home: . Do not dump into Polyglot-Dev/Web/.
§II — Why a placeholder beats an overload pile
Cherny’s filter walk shows the trap. A number-only signature works for numbers. Overload for strings. Overload for object[] and watch property access die: object carries no fields. The fix is a generic type parameter: a placeholder that ties the array element, the predicate argument, and the return element together:
type Filter = {
<T>(array: T[], f: (item: T) => boolean): T[];
};
const filter: Filter = (array, f) => {
const result = [];
for (let i = 0; i < array.length; i++) {
const item = array[i];
if (f(item)) result.push(item);
}
return result;
};
Angle brackets declare the parameter; placement scopes it. Call-site inference usually binds T from the array argument. Explicit bindings are all-or-nothing: annotate every required generic, or none.
map needs two placeholders: T for input members, U for output members. Cherny’s standing advice: use generics when they keep the code general without lying about shape.
§III — Constraints, aliases, and defaults
A bare T accepts anything. A constraint narrows what you may bind:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest([1, 2], [1, 2, 3]); // number[]
longest("hi", "hello"); // string
// longest(10, 100); // error: number has no length
That is Cherny’s constraint idea and the Handbook’s extends form: T must be assignable to the bound.
Generics also live on type aliases, interfaces, and classes. Defaults fill optional type arguments when inference has nothing to chew on (type MyEvent<Type extends string = string> = ... in Cherny’s defaults section). Prefer inference; reach for defaults when the API has a sensible fallback.
§IV — Record, keyof, and mapped types
Cherny implements Record with a mapped type:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
Record<Keys, Type> builds an object whose keys are Keys and whose values are Type. Unlike a plain index signature, Keys can be a string-literal union, so missing days fail loudly.
Mapped types generalize that reshape:
type MyMappedType = {
[Key in UnionType]: ValueType;
};
You map over keys. Pair with keyof when you need “every key of this type” as a union. That is the engine under several utilities.
§V — Utility types you will reach for daily
The Handbook’s global utilities (cite the live page; do not memorize the full list today):
| Utility | Move |
|---|---|
Partial<Type> | Every property optional — good for patches / updates. |
Required<Type> | Every property required — opposite of Partial. |
Readonly<Type> | Every property readonly — assignment fails in the type checker. |
Pick<Type, Keys> | Keep only listed keys. |
Omit<Type, Keys> | Drop listed keys (Handbook; post-dates Cherny 2019). |
Record<Keys, Type> | Object with constrained keys and a shared value type. |
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: number;
}
type TodoPreview = Pick<Todo, "title" | "completed">;
type TodoUpdate = Partial<Todo>;
type TodoPublic = Omit<Todo, "createdAt">;
const cats: Record<"miffy" | "boris", { age: number }> = {
miffy: { age: 4 },
boris: { age: 7 },
};
Pick when the public surface is a named subset. Omit when the secret is easier to name than the keep-list. Partial when callers may send any subset. Record when keys are a known union and values share one shape.
§VI — One complete proof
- Write a generic
pluck<T, K extends keyof T>(items: T[], key: K): T[K][](or Cherny-stylefilter<T>) and call it so inference bindsT. - Constrain a second generic with
extendsso a bad call fails in the checker. - From one interface, form
Partial,Pick, andOmitaliases and assign legal objects to each. - Build a
Record<"a" | "b", number>and confirm a missing key is an error. - Say aloud: generics share a placeholder; utilities reshape existing object types without retyping fields.
When those five hold, Topics #9’s selected depth is done.
§VII — Closing
Generics keep one implementation honest across element types. Utilities keep object shapes honest when you patch, preview, or index by a known key set. Cherny teaches the placeholder and the mapped-type engine; the Handbook names the global toolkit including Omit. Bun rows 5–8 stay deferred. Next Primary TypeScript depth follows the syllabus after #9 once a teammate seats it.
Done-criteria: Can write a generic function/type, constrain with extends, and apply Partial / Pick / Omit / Record with intent.