TypeScript utility types I use every day
TypeScript’s built-in utility types are not trivia for interviews—they are the default vocabulary for shaping data at boundaries: HTTP payloads, form state, props derived from a larger domain model, and configuration objects.
This post is a practical tour: what I reach for first, how I combine utilities, and when I escalate to mapped or conditional types.
Pick and Omit at HTTP boundaries
APIs return wide objects. UI and internal services often need narrow projections.
type ApiUser = {
id: string;
email: string;
displayName: string;
role: 'admin' | 'member';
mfaEnabled: boolean;
lastLoginAt: string;
};
/** Public profile card */
type PublicProfile = Pick<ApiUser, 'id' | 'displayName'>;
/** Admin user table row — hide MFA internals from non-admin clients */
type AdminRow = Omit<ApiUser, 'email'>;
Pick and Omit stay readable in code review because the intent is nominal: “same shape as ApiUser but only these keys” or “everything except X”.
Omit and unions
Omit<T, K> distributes awkwardly when K is a union in some versions—know your TS version. If you need to drop multiple keys and the compiler complains, prefer:
type WithoutSecrets = Pick<ApiUser, Exclude<keyof ApiUser, 'email' | 'mfaEnabled'>>;
Partial, Required, and Readonly
Partial<T> is the right model for draft entities and multi-step wizards:
type DraftProject = Partial<{
name: string;
repoUrl: string;
defaultBranch: string;
}>;
Before persistence, I often define a separate CreateProjectInput that uses Required on the subset that the backend demands:
type CreateProjectInput = Required<Pick<DraftProject, 'name' | 'repoUrl'>> &
Partial<Pick<DraftProject, 'defaultBranch'>>;
Readonly<T> shines for configuration objects that should not be mutated after creation:
const routes = {
home: '/',
blog: '/blog',
} as const satisfies Readonly<Record<string, `/${string}`>>;
Record for dictionaries
When keys are a finite union, Record is clearer than index signatures:
type Locale = 'en' | 'pt';
const copy: Record<Locale, { title: string; description: string }> = {
en: { title: 'Home', description: 'Welcome' },
pt: { title: 'Início', description: 'Bem-vindo' },
};
If you need partial coverage during migration, Partial<Record<Locale, Messages>> documents that some locales may be missing.
satisfies instead of widening
Before satisfies, developers often chose between:
as const(narrow literals, but awkward when you need to satisfy a wider interface), or- explicit annotations (wide types, lost literal precision).
type ThemeColor = 'slate' | 'zinc' | 'stone';
const theme = {
primary: 'slate',
secondary: 'zinc',
} satisfies Record<'primary' | 'secondary', ThemeColor>;
// theme.primary is literal 'slate', not ThemeColor
This keeps inference tight while still catching typos against ThemeColor.
Mapped types for DRY transformations
When every key needs the same transformation, a mapped type beats repeated Pick:
type Nullable<T> = { [K in keyof T]: T[K] | null };
type User = { id: string; name: string };
type NullableUser = Nullable<User>;
Common patterns:
Readonly<T>as{ readonly [K in keyof T]: T[K] }DeepReadonly<T>for nested structures (recursive mapped type—use sparingly, compile-time cost grows)
Conditional types: use sparingly
Conditional types (T extends U ? X : Y) are powerful for function overloads and library code. In application code, they often hurt readability.
I reach for them when:
- Wrapping untyped external data with a discriminated narrowing.
- Building small type-level helpers (
ReturnType,Parametersare built-ins—prefer those first).
Practical decision tree
- Subset or exclusion of keys? →
Pick/Omit. - All keys optional or readonly? →
Partial/Readonly. - Known key union to values? →
Record. - Preserve literals but check assignability? →
satisfies. - Same transform on every property? → mapped type.
- “If this shape, then that type”? → conditional type (last resort in app code).
Conclusion
Utility types are compression for intent: they let the compiler enforce invariants at module boundaries without repeating field lists. Mastering Pick, Omit, Partial, Record, and satisfies covers most day-to-day work; escalate to mapped and conditional types when the duplication cost clearly outweighs readability.