5 TypeScript Utility Types That Save Hours Every Week
3 min read
Most TypeScript codebases I audit have the same smell: near-duplicate interfaces.
User, UserUpdate, UserPreview, UserFormValues — four hand-written types drifting
apart one field at a time. Utility types exist to kill exactly this. They're built into
the language, they're one line each, and these five earn their keep weekly.
All examples build on one interface:
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'member';
createdAt: Date;
}Partial — PATCH payloads
Partial<User> makes every field optional. Its natural habitat is update operations,
where the caller sends only what changed:
async function updateUser(id: string, changes: Partial<User>) {
return api.patch(`/users/${id}`, changes);
}
updateUser('u_1', { name: 'New Name' }); // ✓
updateUser('u_1', { nmae: 'typo' }); // ✗ compile errorWithout it, teams either write a duplicate UserUpdate interface (drift risk) or type
the payload as any (no checking at all). Partial gives you the update type derived
from the source of truth — change User, and every update site stays correct.
Pick and Omit — shaping API responses
Pick keeps only the listed fields; Omit removes them. They shine anywhere a subset of
a type crosses a boundary:
// A list endpoint returns lightweight previews:
type UserPreview = Pick<User, 'id' | 'name'>;
// A public API response must never include internal fields:
type PublicUser = Omit<User, 'createdAt'>;The rule of thumb I give reviewers: Pick when the subset is small, Omit when the
exclusion is small. Either way, the subset can't silently drift from the real type —
rename name in User and Pick<User, 'name'> fails to compile until you fix it.
Record — typed lookup objects
Record<K, V> types an object as a map from keys to values. It's the fix for the untyped
lookup-table pattern:
const roleLabels: Record<User['role'], string> = {
admin: 'Administrator',
member: 'Team member',
};Two things happen here that a plain object literal doesn't give you. Add a new role to
the union — say 'viewer' — and this object fails to compile until you add its label.
Typo a key and you're caught immediately. I use this constantly for status→color maps,
route→permission tables, and config keyed by environment.
ReturnType — stop duplicating what functions already know
ReturnType<typeof fn> extracts a function's return type. It removes the need to
hand-maintain a type that some function already defines implicitly:
function buildSearchParams(query: string, page: number) {
return { q: query.trim(), page, perPage: 20 };
}
// One source of truth — no separate SearchParams interface to maintain:
type SearchParams = ReturnType<typeof buildSearchParams>;
function serialize(params: SearchParams): string {
return new URLSearchParams(params as never).toString();
}This is especially valuable with factory functions and store creators, where the return shape is rich and writing it manually would be both tedious and instantly stale.
Quick reference
| Utility | One-liner |
|---|---|
Partial<T> | All fields optional — update/PATCH payloads |
Pick<T, K> | Keep only listed fields — previews, DTOs |
Omit<T, K> | Remove listed fields — strip internal data |
Record<K, V> | Typed lookup object — label maps, config tables |
ReturnType<F> | Extract a function's return type — one source of truth |
The shared theme: derive types instead of duplicating them. Every hand-copied interface is a future bug where two definitions disagree; every derived type updates itself. Once this clicks, you'll start seeing the same idea everywhere in TypeScript — it's the foundation for patterns like discriminated unions and the features I covered in what I actually use every week.