InteractiveFrameworks

Utility types

Derive types from types instead of writing them twice: Omit for what a client sends to create, Partial for what it sends to update, Pick for a summary, and a mapped type of your own. The same idea as NestJS's PartialType and OmitType, which apply these to DTO classes.

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

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:

UtilityGivesTypical use
Partial<T>every property optionalan update body
Required<T>every property requireda fully filled-in config
Readonly<T>every property readonlya value a function promises not to change
Pick<T, 'a' | 'b'>only those propertiesa summary, a public view
Omit<T, 'a'>every property except thosea create body without the id
Record<K, V>an object with keys K and values Va lookup table
ReturnType<typeof f>what f returnsnaming 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.
  • undefined is not a value to write. { ...owner, ...{ phone: undefined } } sets phone to undefined, erasing it. A form that sends every field, blank ones as undefined, 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.

  1. CreateCatDto is every field of a Cat except id.
  2. UpdateCatDto is every field of CreateCatDto, none required.
  3. CatSummary is a Cat's id and name.
  4. Nullable<T> has the same keys as T, each value also allowed to be null.
  5. createCat(id, dto) is the dto with the id added.
  6. applyUpdate(cat, update) returns a new cat with the update applied: the cat passed in is unchanged, a field the update leaves undefined keeps its value, and the id stays the cat's own whatever the update carries.
  7. summarize(cat) returns exactly { id, name }.

When it fails

  • Type 'false' does not satisfy the constraint 'true' on Create or Update: the type is written out rather than derived, and misses microchip or keeps id.
  • Cannot assign to read only property 'age': applyUpdate changed 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, Partial and Pick.
  • 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.