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
PartialorPickfrom 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