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.
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
| Utility | Gives 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
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 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.
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
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.
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
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<Door>>; // Door
type B = Unwrap<Door>; // Door
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
type Event = `door:${"created" | "deleted"}`; // "door:created" | "door:deleted"
type CssVar = `--${string}`;
function on(event: Event) { … }
on("door:created"); // fine
on("door:updated"); // Error