Lesson 15 of 20

Utility Types

Derived Types Beat Copied Types

Real applications need several views of the same shape. You have a Student as stored in the database; the version a form submits has no id; the version an update accepts has every field optional; the version you send to the browser has the private fields stripped out. Writing four interfaces means four places to edit when one field changes, and one of them will be forgotten.

Utility types solve this by deriving the variants from a single source of truth. They are ordinary generic type aliases that ship with TypeScript, built from the type operators you have already met, and using them is no harder than calling a function.

Start with the three that transform every property at once. Partial<T> makes them all optional, which is the natural type for a patch or update payload. Required<T> removes every ?, useful for the settings object you get after merging user values over defaults. Readonly<T> marks them all read-only, which is a good habit for configuration and for anything shared between modules.

All three are shallow. Partial<Student> makes the top-level fields optional and leaves any nested object exactly as it was, and Readonly<T> protects the top level only. There is no built-in deep version, and writing a correct one is harder than it looks — if you need it, use a library rather than improvising.

Example
interface Student {
  id: number;
  name: string;
  email: string;
  marks: number;
  address: { city: string; pincode: string };
}

// Every field optional — the natural shape of an update
type StudentUpdate = Partial<Student>;

function update(id: number, changes: StudentUpdate): void { /* ... */ }

update(1, { marks: 92 });          // OK
update(1, { marks: "92" });        // ERROR: types still checked

// Every field required
interface Settings { theme?: string; fontSize?: number }
type ResolvedSettings = Required<Settings>;
// { theme: string; fontSize: number }

// Every field read-only — but only at the top level
type FrozenStudent = Readonly<Student>;

const s: FrozenStudent = {
  id: 1, name: "Ananya", email: "a@example.com", marks: 88,
  address: { city: "Pune", pincode: "411001" }
};

s.name = "Rahul";             // ERROR: read-only
s.address.city = "Kochi";     // allowed! Readonly is shallow.
Notes
  • Partial has a design consequence worth thinking about: Partial<T> permits the empty object, so an update function typed this way accepts a call that changes nothing. If at least one field must be supplied, Partial alone will not enforce it.

Pick and Omit — and the Typo Omit Ignores

Pick<T, Keys> builds a new type from a chosen subset of properties. Omit<T, Keys> does the opposite, taking everything except the named ones. They cover the extremely common need to expose part of a type: a public profile without the password hash, a list row with only the three fields the table shows, a create payload without the server-generated id.

Choose between them by asking which list will stay shorter as the type grows. Pick is an allow-list — new fields added to Student stay out of the derived type until you add them, which is what you want for anything sent to a browser. Omit is a deny-list, so new fields are included automatically. For anything security-relevant, prefer Pick: forgetting to add a field is harmless, forgetting to exclude one is a leak.

There is a genuine sharp edge here. Pick checks the key names against the type and errors on a typo. Omit does not — its key parameter accepts any key name at all, so Omit<Student, "pasword"> compiles cleanly and removes nothing. The type looks correct and quietly still contains the field you meant to drop. When an Omit does not seem to be working, check the spelling first.

One more limitation: Omit does not distribute over a union. Applying it to a discriminated union collapses the members into a single merged shape and the narrowing you relied on stops working.

Example
interface Student {
  id: number;
  name: string;
  email: string;
  passwordHash: string;
  marks: number;
}

// Allow-list: safest for anything leaving your server
type PublicStudent = Pick<Student, "id" | "name" | "marks">;

// Deny-list: convenient, but new fields are included by default
type SafeStudent = Omit<Student, "passwordHash">;

// The create payload: no server-generated fields
type CreateStudent = Omit<Student, "id" | "passwordHash">;

function create(input: CreateStudent): Student { /* ... */ }

// Pick catches a typo
type Broken = Pick<Student, "nmae">;
// ERROR: Type '"nmae"' does not satisfy the constraint 'keyof Student'.

// Omit does not
type StillLeaking = Omit<Student, "pasword">;
// No error. passwordHash is still in the type.
Notes
  • A useful habit for API types: define the stored shape once, then derive Create, Update and Public variants from it with Omit, Partial and Pick. One edit to the base type updates all four.

Record: Objects Used as Lookups

Record<Keys, Value> builds an object type from a set of keys and a single value type. Record<string, number> is a dictionary of any string key to a number — the same thing an index signature gives you, in a shorter form.

It becomes far more interesting when the key type is a union of literals. Record<Role, string[]> does not just permit those three keys; it requires all of them. Miss one and the compiler tells you. That turns a permissions table, a label map or a colour-per-status lookup into something that cannot silently lose an entry, and — combined with a union type you already have — it means adding a new role produces an error in every table that needs updating.

Reading from a Record with a general string key has the same gap as array indexing: the result is typed as present even when the key is missing. The noUncheckedIndexedAccess option closes it here too, and this is one of the better arguments for turning that option on.

Example
type Role = "admin" | "teacher" | "student";

// Every key is required — a missing role is a compile error
const permissions: Record<Role, string[]> = {
  admin: ["read", "write", "delete"],
  teacher: ["read", "write"],
  student: ["read"]
};

// Remove one entry:
// ERROR: Property 'student' is missing in type ... but required in type
//        'Record<Role, string[]>'.

// An open dictionary — same as an index signature
type MarksByCity = Record<string, number>;

const averages: MarksByCity = { pune: 72.4, kochi: 68.1 };
const missing = averages.indore;
// typed number, but undefined at runtime unless
// noUncheckedIndexedAccess is enabled

// Combining utilities is normal
type OptionalPermissions = Partial<Record<Role, string[]>>;

Filtering Unions: Exclude, Extract, NonNullable

Three utilities operate on unions rather than object shapes. Exclude<T, U> removes from the union T everything assignable to U. Extract<T, U> keeps only those members. NonNullable<T> is the common case of Exclude, dropping null and undefined.

The everyday use is deriving a narrower set of options from a wider one. If OrderStatus has five members and one screen only handles the three that are not terminal, Exclude<OrderStatus, "cancelled" | "delivered"> gives you that subset without writing the members out again — and if a sixth status appears, it is included automatically.

NonNullable is most useful in a function signature that promises a value after a check, or to derive a type from a nullable field: NonNullable<Student["mentor"]> gives you the mentor type without the undefined.

Remember these operate on union members, not on object properties. Exclude<Student, "email"> does nothing useful, because Student is a single type rather than a union — that job belongs to Omit. Mixing the two up is a common early mistake and produces a type that silently equals the original.

Example
type OrderStatus = "placed" | "packed" | "shipped" | "delivered" | "cancelled";

// Derive a subset instead of repeating members
type ActiveStatus = Exclude<OrderStatus, "delivered" | "cancelled">;
// "placed" | "packed" | "shipped"

type FinalStatus = Extract<OrderStatus, "delivered" | "cancelled">;
// "delivered" | "cancelled"

function advance(status: ActiveStatus): OrderStatus { /* ... */ }

advance("packed");      // OK
advance("delivered");   // ERROR: not assignable to 'ActiveStatus'

// NonNullable, and deriving from a property
interface Student {
  name: string;
  mentor?: { id: number; name: string };
}

type Mentor = NonNullable<Student["mentor"]>;
// { id: number; name: string } — the undefined is gone

// Exclude works on unions, not on properties
type Wrong = Exclude<Student, "name">;   // still Student — use Omit instead
Notes
  • These three, plus Omit and Record, are the ones you will use most weeks. The rest of the list is worth skimming once so you recognise the names when you meet them.

Types Taken From Functions and Promises

Sometimes the source of truth is a function that already exists, and writing its shapes out again by hand guarantees they will drift. Three utilities read the types straight off it.

ReturnType<typeof fn> gives the function's return type. Note the typeof: fn is a value, and you need its type before a type operator can be applied. Parameters<typeof fn> gives the parameter list as a tuple, so Parameters<typeof fn>[0] is the first parameter's type. That pattern is how you type a wrapper that forwards arguments to another function without repeating its signature.

Awaited<T> unwraps a promise, giving you the type a caller actually receives after await. Combined with ReturnType, it turns an async function into the type of its resolved value — the standard way to derive a data type from the loader that fetches it, so the type can never disagree with the code.

Use these where the function genuinely is the source of truth. If the type is the contract and the function must match it, write the type first and annotate the function; deriving in the wrong direction hides mistakes rather than catching them.

Example
async function loadStudent(id: number) {
  const res = await fetch(`/api/students/${id}`);
  const data = (await res.json()) as { id: number; name: string; marks: number };
  return data;
}

// The return type, without writing it twice
type LoadResult = ReturnType<typeof loadStudent>;
// Promise<{ id: number; name: string; marks: number }>

type StudentData = Awaited<ReturnType<typeof loadStudent>>;
// { id: number; name: string; marks: number }

function render(student: StudentData): string {
  return `${student.name}: ${student.marks}`;
}

// Parameters gives a tuple
type LoadArgs = Parameters<typeof loadStudent>;   // [id: number]
type IdType = Parameters<typeof loadStudent>[0];  // number

// Forwarding arguments without repeating the signature
function logged(...args: Parameters<typeof loadStudent>) {
  console.log("loading", args[0]);
  return loadStudent(...args);
}

How They Work, and When to Stop

None of these utilities is built into the compiler as a special case — they are written in TypeScript, in the standard library, using mapped types. A mapped type loops over the keys of a type and produces a new property for each one, and once you have seen the pattern, the utilities stop feeling like magic.

Partial<T> is three lines: for every key in T, produce the same key marked optional. Readonly<T> is the same with readonly instead. The modifiers can be removed as well as added, with a minus sign — which is exactly how Required<T> is written.

Knowing this lets you write the small custom helper your project needs when nothing built in fits. Keep those helpers rare and simple. Type-level programming is genuinely powerful, and it is also where TypeScript codebases go to become unreadable: a clever conditional type that nobody on the team can modify is worse than three plainly written interfaces.

A reasonable line to hold: use the built-in utilities freely, write a simple mapped type when it removes real duplication, and stop before anything needs a comment explaining how the type works.

Example
// This is how Partial is defined in TypeScript's own library
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Readonly is the same idea with a different modifier
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// A minus sign removes a modifier — this is how Required works
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

// A small custom helper: make some keys optional, keep the rest
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface Student {
  id: number;
  name: string;
  marks: number;
}

type DraftStudent = PartialBy<Student, "marks">;
// id and name required, marks optional

const draft: DraftStudent = { id: 1, name: "Meera" };   // OK
Notes
  • Other utilities worth recognising when you meet them: Uppercase, Lowercase, Capitalize and Uncapitalize for string literal types, InstanceType for the instance type of a class, and ConstructorParameters for a constructor's arguments.
Ask AI