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
| Utility | What 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