Skip to content
BloGrove
web dev

TypeScript Generics and Utility Types: The Practical Guide

Generics and utility types through real refactors — when <T> earns its complexity, which built-ins matter, and the patterns codebases actually use.

BBloGrove Editorial3 min read
TypeScript Generics and Utility Types: The Practical Guide

TypeScript's basic types take a weekend; generics and utility types separate casual users from people who can model real domains. They're also where interview coding exercises live. The good news: both topics reduce to one idea applied repeatedly — types can be functions of other types. This guide builds that idea from concrete refactors.

Generics: types as parameters#

Start with the problem they solve:

function firstOrNull(items: any[]): any {
  return items[0] ?? null;
}
const name = firstOrNull(["a", "b"]); // any — all safety gone

any works but erases everything TypeScript knows. Generics let a function say "I'll accept some type, and I'll keep track of which":

function firstOrNull<T>(items: T[]): T | null {
  return items[0] ?? null;
}
const name = firstOrNull(["a", "b"]); // string | null — inferred!

<T> declares a type variable; each call binds it concretely (usually inferred — write it explicitly only when inference fails). One function, full type safety for every input type. That's the entire concept.

Where generics actually earn complexity#

API clients and wrappers:

async function getJson<T>(url: string): Promise<T> {
  const res = await fetch(url);
  return res.json();
}
const user = await getJson<User>("/api/user/1");

Reusable component props (React is generic machinery end to end):

function Select<T extends { id: string }>(props: {
  items: T[];
  value: T | null;
  onChange: (item: T) => void;
}) { /* ... */ }

Constraining with extends — the workhorse move. T extends { id: string } says "any type that at least has an id," keeping flexibility while allowing .id access inside. Unconstrained T lets you do nothing with values except pass them along; constraints unlock operations:

function pickKeys<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  /* ... */
}

That signature reads like a sentence once fluent: "given any object and keys that exist on it, return just those fields." Interviews love it because it compresses three ideas — generics, keyof, mapped results — into one line.

The utility types that matter#

TypeScript ships transformations covering 90% of daily needs:

Utility Does what Typical use
Partial<T> all fields optional update payloads
Pick<T, K> / Omit<T, K> subset of fields API responses vs forms
Record<K, V> dictionary type lookup tables, enums-as-objects
ReturnType<F> function's return type derive types instead of duplicating
Readonly<T> immutable view config, shared state

The pattern behind most good TypeScript architecture: define the source-of-truth shape once, derive everything else:

interface User {
  id: string;
  email: string;
  passwordHash: string;
  createdAt: Date;
}

type PublicUser = Omit<User, "passwordHash">;
type UserDraft = Partial<Pick<User, "email" | "createdAt">>;
type UsersById = Record<string, User>;

One interface, three derived types, zero drift between them. Hand-maintained duplicates of these shapes are how codebases accumulate lies.

Discriminated unions: the pattern hiding under everything#

type RequestState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };

function render(state: RequestState<User>) {
  if (state.status === "success") {
    console.log(state.data.name); // data exists ONLY here
  }
}

A literal tag field (status) plus a union makes illegal states unrepresentable — you literally cannot access .data without narrowing to success first. Async UI, form states, API envelopes: nearly every messy boolean-flag situation becomes cleaner this way, and every exhaustive switch gets compiler-checked completeness. If you learn one intermediate pattern, make it this one.

What interviews actually ask#

  • Write a generic debounce/pick/pluck with constraints
  • Implement Partial or Pick from scratch (mapped types + key remapping)
  • Explain inference: why explicit <T> is rarely needed at call sites
  • Model a discriminated union for a described domain

Preparation is mostly reps in the TS Playground — same pattern-drilling logic as algorithms, different material. Fluency checkpoint: when you next duplicate an interface "just slightly different," stop and derive it instead. That reflex is the skill.

Related: React's rendering model · Next.js data fetching · 12-week plan

Enjoyed this article?

Share it with your network.

Share

Keep reading

The React Rendering Model, Explained Once and Properly
web dev

The React Rendering Model, Explained Once and Properly

How React decides what to re-render — components, state triggers, reconciliation, keys — and a mental model that avoids perf bugs and over-memoization.

3 min read
CSS Flexbox vs Grid: Choosing the Right Layout Engine
web dev

CSS Flexbox vs Grid: Choosing the Right Layout Engine

Deciding between Flexbox and Grid in practice — how their mental models differ, plus recipes for navbars, cards, split pages, and sticky footers.

4 min read