Files
What you'll learn
- Derive types with Omit, Pick, Partial, Required, Readonly and Record, so one change to a model reaches every type built from it
- Write a mapped type over keyof T
- Apply a partial update without mutating the original or letting undefined erase a value
Utility types
An API describes the same thing several times. The stored cat has an id; the body that creates one must not, since the server assigns it; the body that updates one has every field optional; the list shows only an id and a name. Written out by hand, those are four types that drift apart the first time someone adds a microchip to one and forgets the others. TypeScript can derive types from types instead, so the model is written once and everything else follows from it. NestJS does the same for DTO classes with PartialType(CreateCatDto) and OmitType(Cat, ['id']); this lesson is the TypeScript underneath.
The built-in utilities
Each takes a type and gives a new one:
| Utility | Gives | Typical use |
|---|---|---|
Partial<T> | every property optional | an update body |
Required<T> | every property required | a fully filled-in config |
Readonly<T> | every property readonly | a value a function promises not to change |
Pick<T, 'a' | 'b'> | only those properties | a summary, a public view |
Omit<T, 'a'> | every property except those | a create body without the id |
Record<K, V> | an object with keys K and values V | a lookup table |
ReturnType<typeof f> | what f returns | naming a type you did not write |
They compose: Partial<Omit<Owner, 'id'>> is "any of an owner's fields except its id". And they stay linked to the source: add a field to Owner and every derived type has it too.
interface Owner { id: number; name: string; email: string; phone?: string }
type OwnerCard = Pick<Owner, 'name' | 'email'>; // { name: string; email: string }
type NewOwner = Omit<Owner, 'id'>; // { name; email; phone? }
Pick and Omit keep each property's modifiers: phone is still optional in NewOwner.
Mapped types
The utilities are ordinary TypeScript, and you can write your own. A mapped type walks the keys of a type and says what each property becomes:
type Stringified<T> = { [K in keyof T]: string };
// Stringified<{ id: number; paid: boolean }> is { id: string; paid: string }
[K in keyof T] visits each key; T[K] is that property's type, so a mapped type can transform it rather than replace it. Partial<T> itself is just { [K in keyof T]?: T[K] }. A mapped type over keyof T also keeps each property's ? and readonly, which is why it is the right tool for "the same shape, changed".
Types exist only at compile time
A derived type constrains what the compiler accepts, and is erased from the JavaScript that runs. UpdateOwner forbids { id: 3 } in code you write, but a JSON body parsed at runtime can contain anything, and a cast (as UpdateOwner) believes it. So code that must hold whatever arrives, "the id never changes", enforces it in the code, not only in the type. This is the same reason NestJS pairs its DTO classes with runtime validation.
Partial updates, done safely
An update is a merge, and two details make the difference between right and nearly right:
- Do not change the original. Build a new object from the old one and the changes:
{ ...owner, ...changes }. Later spreads win, so the order says which values take precedence. undefinedis not a value to write.{ ...owner, ...{ phone: undefined } }setsphonetoundefined, erasing it. A form that sends every field, blank ones asundefined, would wipe data. Drop those entries first:
const defined = Object.fromEntries(Object.entries(changes).filter(([, v]) => v !== undefined));
Your task
The work is in dto.ts. Derive every type from Cat, so a field added to Cat reaches them without an edit.
CreateCatDtois every field of aCatexceptid.UpdateCatDtois every field ofCreateCatDto, none required.CatSummaryis aCat'sidandname.Nullable<T>has the same keys asT, each value also allowed to benull.createCat(id, dto)is the dto with the id added.applyUpdate(cat, update)returns a new cat with the update applied: the cat passed in is unchanged, a field the update leavesundefinedkeeps its value, and the id stays the cat's own whatever the update carries.summarize(cat)returns exactly{ id, name }.
When it fails
Type 'false' does not satisfy the constraint 'true'onCreateorUpdate: the type is written out rather than derived, and missesmicrochipor keepsid.Cannot assign to read only property 'age':applyUpdatechanged the frozen cat passed in. Build a new object.- "keeps a field the update leaves undefined" sees
color: undefined: undefined entries were spread over the cat. - "never changes the id" gets 99: the changes were spread after the id, or included it.
Remember
- Write a model once; derive create, update and view types with
Omit,PartialandPick. - A mapped type,
{ [K in keyof T]: … }, transforms every property and keeps its modifiers. - Types are erased at runtime: rules that must hold for any input are enforced in code.
- Merge updates into a new object, skip
undefined, and put what must not change last.
Stuck? Show a hint
CreateCatDto is Omit<Cat, 'id'>, UpdateCatDto is Partial of that, CatSummary is Pick<Cat, 'id' | 'name'>. A mapped type is { [K in keyof T]: … }, where T[K] is the type of each property. For applyUpdate, drop the entries whose value is undefined (Object.entries, filter, Object.fromEntries), spread the cat then the changes into a new object, and put the cat's own id last so nothing can replace it.
Press Run tests to start the app. Its log appears here.Tests
- dto.spec.tsrun to see its tests