Skip to main content
CodeOath
← All posts

TypeScript63 min total · 21 parts

TypeScript Fundamentals: Types, Interfaces, Generics, and Why It Catches Bugs Before Runtime

Part 13 of 21 · ~2 min

Utility Types

A lot of the types you actually want in a real codebase are small transformations of a type you already have — "this, but everything optional," or "this, minus one field." TypeScript ships a set of these as built-ins, so you rarely have to hand-write the transformation yourself. The queue also needs a field only reviewers can see — private notes on a candidate — so let's add that before working through what each utility actually produces from it:

interface Applicant {
  id: number;
  name: string;
  email: string;
  stage: Stage;
  internalNotes: string;
}

type ApplicantPatch = Partial<Applicant>;                 // every field becomes optional
type ApplicantComplete = Required<Applicant>;              // the reverse — every field becomes required
type ApplicantSnapshot = Readonly<Applicant>;               // every field becomes readonly
type PublicApplicant = Omit<Applicant, "internalNotes">;    // Applicant, minus the reviewer-only field
type ApplicantPreview = Pick<Applicant, "id" | "name">;     // only id and name survive
type PipelineColumns = Record<Stage, Applicant[]>;          // one Applicant[] bucket per pipeline stage
type MaybeApplicant = Applicant | undefined;
type DefiniteApplicant = NonNullable<MaybeApplicant>;       // back to plain Applicant, undefined stripped out
UtilityWhat it produces
Partial<T>T, every field optional — a good shape for a "patch" argument
Required<T>T, every field required, even ones T itself marked optional
Readonly<T>T, every field readonly — a compile-time-only lock, nothing at runtime
Pick<T, K>Just the listed keys of T
Omit<T, K>T, minus the listed keys
Record<K, V>An object type with every key from K mapped to V
ReturnType<F>Whatever F returns, without invoking it
Parameters<F>F's parameter list, as a tuple
NonNullable<T>T with null and undefined removed from the union

ReturnType and Parameters are most useful on a function you didn't write yourself — something from a scoring library, or a generated API client — where retyping the signature by hand would just be a second copy waiting to drift out of sync with the first:

function scoreApplicant(a: Applicant, weights: { keywords: number; recency: number }) {
  return { applicantId: a.id, total: weights.keywords + weights.recency };
}

type ScoreParams = Parameters<typeof scoreApplicant>; // [a: Applicant, weights: { keywords: number; recency: number }]
type ScoreResult = ReturnType<typeof scoreApplicant>; // { applicantId: number; total: number }

And ApplicantPatch from the top of the chapter is exactly the right shape for a real update function — a reviewer editing one note shouldn't have to resupply the applicant's id, name, email, and stage just to satisfy the type checker:

function updateApplicant(id: number, patch: Partial<Applicant>): void {
  const applicant = findApplicant(id);
  Object.assign(applicant, patch);
}

updateApplicant(8841, { internalNotes: "Strong on system design, light on testing." });
// legal — Partial made every other field on Applicant optional for this call