Every big object in a real codebase eventually needs a second version of itself. An edit form only wants some fields optional. A public API response wants to drop the one field that is nobody else's business. A vote count wants a lookup table shaped like the object but is not really the object at all. The instinct that has served you fine in plain JS, just write a second interface by hand, is exactly the instinct that gets you two types quietly drifting apart the first time someone adds a field to the original and forgets the copy exists.
Utility types: functions, but for types
TypeScript ships a small set of built in utility types that take a type in and hand a new type
back, the same verbs you already reach for on values. You already write pick these keys and
drop that one on objects and arrays all the time, with .filter, with destructuring, with
Object.entries. Utility types are that same instinct running at the type level instead of the
value level, one type in, one derived type out, computed instead of retyped.
The ES6 you write
function applyNomineePatch(nominee, patch) {
return { ...nominee, ...patch }
}The TS version
interface Nominee {
name: string
age: number
hometown: string
strategy: string[]
}
type NomineePatch = Partial<Nominee>
function applyNomineePatch(nominee: Nominee, patch: NomineePatch): Nominee {
return { ...nominee, ...patch }
}What changed
applyNomineePatch's patch parameter goes from an untyped second argument to Partial<Nominee>, a type generated from Nominee itself rather than a second interface hand-written to match it. Nominee never gets copied anywhere. If a field gets added to Nominee tomorrow, NomineePatch already has it, automatically, with no edit required here.
Partial<Nominee> takes every property Nominee has and makes each one optional, same shape,
every ? added automatically. Nobody wrote { name?: string; age?: number; ... } by hand. The
type was computed from Nominee, once, and it stays computed forever: change Nominee, and
NomineePatch changes with it on the next compile, no second edit anywhere.
Pick and Omit: API surfaces without the copy-paste
Omit and Pick solve the same drift problem from the other direction: instead of making every
field optional, they choose which fields survive into a new type at all.
type PublicNomineeProfile = Omit<Nominee, 'strategy'>
// { name: string; age: number; hometown: string }
type NomineeCard = Pick<Nominee, 'name' | 'hometown'>
// { name: string; hometown: string }Omit<Nominee, 'strategy'> reads as "everything Nominee has, except strategy," which is
exactly the shape you want handing a houseguest's public profile back over an API without leaking
their game strategy notes. Pick<Nominee, 'name' | 'hometown'> reads the opposite way, "only these
fields, nothing else," useful anywhere you want a smaller view of a bigger interface, a summary
card instead of the full record. Both stay in sync with Nominee automatically, same as Partial
did.
Record: the maps you already write, typed
You have written objects used as lookup tables plenty of times, { Chelsie: 2, Angela: 1 }, key
strings mapping to values, no interface anywhere because the keys are not fixed in advance.
Record<K, V> types exactly that shape: any object where the keys are all type K and the values
are all type V.
type VoteCounts = Record<string, number>
// any object shaped like { [voterChoice: string]: number }That single line is the type for every "count things by name" object you have ever written without thinking about its type at all.
ReturnType: never write a function's output type twice
ReturnType<typeof someFunction> pulls a function's return type straight off the function itself,
instead of you writing that same type out again by hand somewhere else in the file.
function scoreNominee(n: Nominee): number {
return n.age + n.strategy.length
}
type NomineeScore = ReturnType<typeof scoreNominee>
// number, derived, not retypedChange scoreNominee to return an object with a breakdown instead of a bare number, and
NomineeScore updates itself on the next compile. No second type to remember, no risk of the copy
going stale.
That is the whole lesson. Now prove it in the comp below.