Utility types

Derive types from the ones you already have, so a change in one place moves everywhere.

09of 14
≈ 12Min read
8Parts

One source of truth

Every one of those follows Door. Add a field and they all gain it; rename one and the compiler points at everything that has to move. Writing them out by hand is how a codebase ends up with four slightly different ideas of what a door is.

the whole idea in four lines
interface Door { id: string; name: string; days: number; price: number }

type NewDoor = Omit<Door, "id">;          // everything but the id
type DoorPatch = Partial<Door>;            // every field optional
type DoorCard = Pick<Door, "name" | "price">;
type DoorId = Door["id"];                  // string

The everyday five

UtilityGives you
Partial<T>Every field optional — a patch, a set of defaults
Required<T>Every field required — after validation
Pick<T, K>Only those fields
Omit<T, K>Everything except those
Record<K, V>An object with those keys and that value type

keyof, typeof and indexed access

the three that build the rest
type DoorKey = keyof Door;                 // "id" | "name" | "days" | "price"
type Price = Door["price"];                 // number
type Field = Door["name" | "price"];        // string | number

const defaults = { finish: "painted", days: 5 };
type Defaults = typeof defaults;            // from a value to its type

type Item = Door[];
type One = Item[number];                    // Door
typeof crosses the border

typeof in a type position reads a value and gives its type. It is how you avoid writing a type that already exists as a constant.

Deriving from functions

Awaited<T> is the one worth remembering: it turns Promise<Door> into Door, however many promises deep.

the return type is already written — use it
function loadDoors() { return { items: [] as Door[], total: 0 }; }

type LoadResult = ReturnType<typeof loadDoors>;
type FirstArg = Parameters<typeof describe>[0];
type Resolved = Awaited<ReturnType<typeof fetchDoor>>;    // unwraps the promise

Unions in and out

filtering a union
type Status = "loading" | "ready" | "error";

type Settled = Exclude<Status, "loading">;      // "ready" | "error"
type Loading = Extract<Status, "loading">;      // "loading"
type Defined = NonNullable<string | null>;      // string

Mapped types

A form is the clearest case: every field arrives as a string and is converted after validation, so Draft<T> describes the in-between state exactly, and follows the model when it changes.

what the built-ins are made of
type Partial<T> = { [K in keyof T]?: T[K] };
type Readonly<T> = { readonly [K in keyof T]: T[K] };

// your own: every field becomes a string, for a form
type Draft<T> = { [K in keyof T]: string };
type Draft = Draft<Omit<Door, "id">>;    // { name: string; days: string; price: string }

// and with a rename
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };

Conditional types, briefly

a type that decides
type Unwrap<T> = T extends Promise<infer U> ? U : T;

type A = Unwrap<Promise<Door>>;   // Door
type B = Unwrap<Door>;            // Door
Where the error messages get long

Conditional and recursive types are powerful and expensive to read. Write one when it removes real duplication, and leave a comment saying what it does in English.

Template literal types

strings, checked
type Event = `door:${"created" | "deleted"}`;   // "door:created" | "door:deleted"
type CssVar = `--${string}`;

function on(event: Event) { … }
on("door:created");     // fine
on("door:updated");     // Error

Practice