Utility Types
The built-in transformations — Partial, Pick, Omit, Record, Required, Awaited and friends — what each is really for, and the point where a chain of them becomes unreadable.
The built-in transformations — Partial, Pick, Omit, Record, Required, Awaited and friends — what each is really for, and the point where a chain of them becomes unreadable.
There are three types describing a photo. One for creating,
without an id. One for updating, with every field optional. One
for what comes back, with timestamps. Somebody adds a caption
field to the main type and forgets two of the three, and the
mismatch surfaces as a runtime failure weeks later.
Types derived from other types cannot drift. By the end of this lesson you will know the transformations that ship with the language, when a derived type is genuinely better than a written one, and the point at which a chain of them stops being readable.
Bad — three hand-written types.
type Photo = {
id: string;
name: string;
size: number;
createdAt: Date;
};
type CreatePhoto = {
name: string;
size: number;
};
type UpdatePhoto = {
name?: string;
size?: number;
};Good — one source, two derivations.
type Photo = {
id: string;
name: string;
size: number;
createdAt: Date;
};
type CreatePhoto = Omit<Photo, "id" | "createdAt">;
type UpdatePhoto = Partial<CreatePhoto>;The first version has the field list written three times. Adding
caption means three edits, and nothing reports the two you
forgot — the types stay valid, they just no longer describe the
same thing. The failure arrives as a field that silently never
gets saved.
In the second, adding caption to Photo updates both derived
types automatically. There is one definition and two views of
it.
type Photo = { id: string; name: string; size: number };Selecting and removing fields:
Pick<Photo, "id" | "name"> // { id: string; name: string }
Omit<Photo, "id"> // { name: string; size: number }Pick names what to keep; Omit names what to drop. Prefer
Omit when the type will grow — new fields are included
automatically, which is usually what you want for a "everything
except the id" type.
Changing optionality and mutability:
Partial<Photo> // every field optional
Required<Photo> // every field required
Readonly<Photo> // every field readonlyPartial is the standard shape for an update payload.
Readonly on a function parameter says you will not modify it.
Building a lookup:
type Status = "pending" | "done" | "failed";
Record<Status, string> // one entry per status, required
Record<string, number> // any string keyThe first form is the useful one, and the reason is
exhaustiveness: adding a status makes every Record<Status, T>
incomplete, so the compiler lists what you forgot. That is the
same guarantee as assertNever in a switch, applied to lookup
tables.
Working with unions:
type Result = Photo | Error | null;
Exclude<Result, null> // Photo | Error
Extract<Result, Error> // Error
NonNullable<Result> // Photo | ErrorReading from functions and promises:
type Loader = (id: string) => Promise<Photo>;
ReturnType<Loader> // Promise<Photo>
Awaited<ReturnType<Loader>> // Photo
Parameters<Loader> // [id: string]Awaited unwraps a promise, including nested ones. These four
are how you type a wrapper around a function you did not write —
particularly useful when a library exports a function but not
the type of what it returns.
Two operators are the foundation for all of the above.
keyof Photo // "id" | "name" | "size"
Photo["name"] // string
Photo["id" | "name"] // stringkeyof gives the union of property names; the indexed access
gives the type of a property.
And typeof in a type position takes the type of a value:
const DEFAULTS = {
limit: 100,
dryRun: false,
} as const;
type Config = typeof DEFAULTS;
// { readonly limit: 100; readonly dryRun: false }
type ConfigKey = keyof typeof DEFAULTS; // "limit" | "dryRun"That combination — keyof typeof — is the standard way to get a
union of keys from an object you defined as a value. It appears
constantly, and it is the pattern behind the as const array
from the enums lesson:
const STATUSES = ["pending", "done", "failed"] as const;
type Status = (typeof STATUSES)[number];[number] reads "the type at any numeric index", which for a
readonly tuple is the union of its elements. One declaration
gives you a runtime list and a compile-time union.
Three surprises, each of which costs an afternoon the first time.
Omit does not check its keys
A misspelled key removes nothing, and you get the original type back with no complaint.
Omit flattens a union
Applied to a discriminated union it merges the members instead of distributing over them, so the narrowing you built the union for is gone.
Partial is one level deep
The outer fields become optional. Everything nested inside them stays as required as it ever was.
The first one, in full:
type Bad = Omit<Photo, "nmae">; // no error - just returns PhotoOmit accepts any string, so a typo silently removes nothing.
Pick does check, because it must produce those keys. If that
matters, a checked version is three lines:
type StrictOmit<T, K extends keyof T> = Omit<T, K>;And the second, which is the one that bites hardest because the result still looks like a type:
type Upload =
| { status: "done"; url: string }
| { status: "failed"; error: string };
type Bad = Omit<Upload, "status">; // not what you wantDistributing needs a conditional type, which is the advanced course.
And the third:
type Config = { server: { host: string; port: number } };
type P = Partial<Config>; // server is optional; host is notA recursive version exists and is a conditional type. Reach for it deliberately, because deeply-partial types make errors much harder to read.
Derivation has a cost: an error message shows the computation rather than the shape.
Type '{ name: string }' is not assignable to type
'Partial<Omit<Photo, "id" | "createdAt">>'A reader now has to evaluate that in their head to know what was expected. One layer is fine and two is usually the limit.
Bad — a chain nobody can read.
type Payload = Partial<
Omit<Pick<Photo, "name" | "size" | "tags">, "tags">
>;Good — named steps, or just written out.
type EditableFields = Pick<Photo, "name" | "size">;
type Payload = Partial<EditableFields>;The first is one expression doing three things, and every error message mentioning it is that whole expression. Naming the intermediate step costs one line and makes both the type and its errors legible.
The judgement: derive when the types must stay in step; write it out when the relationship is incidental. A create payload that genuinely tracks its entity should be derived. A type that happens to have two fields in common with another is better written, because tying them together invents a relationship that does not exist.
THE PRINCIPLE
two hand-written types drift; a derived one cannot
adding a field updates every derivation automatically
FIELDS
Pick<T, "a" | "b"> keep these
Omit<T, "a"> drop these <- prefer when T will grow
Partial<T> all optional
Required<T> all required
Readonly<T> all readonly
LOOKUPS
Record<Union, V> one entry per member - REQUIRED
adding a member -> an error listing it
Record<string, V> any key
UNIONS
Exclude<U, X> Extract<U, X> NonNullable<U>
FUNCTIONS AND PROMISES
ReturnType<F> Parameters<F> Awaited<P>
how to type a wrapper around something you did not write
THE FOUNDATION
keyof T the union of property names
T["name"] the type of a property
typeof value the type OF A VALUE, in a type position
keyof typeof obj the standard "keys of this object" idiom
(typeof ARR)[number] the union of a readonly tuple's elements
SURPRISES
Omit<T, "nmae"> no error - accepts any string, removes nothing
Omit on a union FLATTENS it, destroying the discriminant
Partial one level deep only
READABILITY
one layer fine, two usually the limit
name the intermediate step rather than nesting three deep
error messages show the EXPRESSION, not the shape
Expand<T> forces evaluation at a public boundary
derive when types must stay in step
write it out when the overlap is incidentalYou can now derive one type from another so they cannot fall out of step, and you know the three utilities whose behaviour is surprising. The habit worth adopting is deriving create and update payloads from the entity — it is the most common place hand-written types drift, and the failure is a field that silently never saves.
Next is Testing TypeScript, which asks what tests look like when the checker has already covered a category of bug. Typing fixtures without fighting them, and asserting on types themselves so a refactor cannot quietly widen an API.
Before you move on, find a pair of types in your code where one restates fields of another, and derive the second from the first. Then add a field to the source and confirm the derived type picked it up. That is the drift you no longer have to police.