The TypeScript utility types I reach for every week
TypeScript ships a standard library of type transformers. These are the five I use constantly and the one I keep forgetting.
TypeScript's built-in utility types are underused. Not because developers do not know they exist — the docs cover them — but because they are listed alphabetically, and alphabetical order is a terrible way to learn things.
Here are the ones I reach for most, ordered by how often I need them.
#Partial<T> — all fields optional
When you want a version of a type where nothing is required:
type User = {
id: string;
name: string;
email: string;
};
type UserUpdate = Partial<User>;
// { id?: string; name?: string; email?: string; }
async function updateUser(id: string, fields: Partial<Omit<User, 'id'>>) {
return db.users.update(id, fields);
}I use this almost exclusively for update/patch operations. Without the Omit, callers can pass id in the update body — which is rarely what you want.
#Pick<T, K> and Omit<T, K> — surgical selection
Pick keeps only the fields you name. Omit keeps everything except the fields you name. They are inverses.
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: string; name: string; }
type UserWithoutId = Omit<User, 'id'>;
// { name: string; email: string; }The rule I follow: use Pick when you want a small subset, use Omit when you want almost everything. If you are picking more fields than you are omitting, you have the wrong one.
#Record<K, V> — typed dictionaries
When you want an object with a specific key type and value type:
type OrderStatus = 'pending' | 'paid' | 'shipped' | 'cancelled';
type StatusLabel = Record<OrderStatus, string>;
const labels: StatusLabel = {
pending: 'Awaiting payment',
paid: 'Payment received',
shipped: 'On its way',
cancelled: 'Order cancelled',
};The exhaustive constraint is the key benefit. If you add 'refunded' to OrderStatus, TypeScript will error on every Record<OrderStatus, ...> that does not handle it. The compiler forces you to update every callsite.
#ReturnType<T> and Parameters<T> — extract from functions
When you need the type of a function's return value or arguments without importing a separate type:
async function getUser(id: string) {
return db.users.findById(id);
}
type User = Awaited<ReturnType<typeof getUser>>;
// Resolves the Promise and extracts the inner type
type GetUserArgs = Parameters<typeof getUser>;
// [id: string]I use ReturnType constantly for functions that return complex objects. Instead of defining a User type separately and keeping it in sync with the function, I derive it. The function is the source of truth.
#NonNullable<T> — strip null and undefined
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>;
// UserMore useful in generic contexts:
function compact<T>(arr: (T | null | undefined)[]): NonNullable<T>[] {
return arr.filter((x): x is NonNullable<T> => x != null);
}
const users = compact([getUser(1), null, getUser(2), undefined]);
// users: User[]#The one I keep forgetting: Extract<T, U> and Exclude<T, U>
These filter union types. Extract keeps members assignable to U; Exclude removes them.
type Status = 'pending' | 'active' | 'inactive' | 'deleted';
type ActiveStatus = Extract<Status, 'active' | 'inactive'>;
// 'active' | 'inactive'
type VisibleStatus = Exclude<Status, 'deleted'>;
// 'pending' | 'active' | 'inactive'The reason I forget them: I always try to use Pick/Omit on a union first, which does not work. Extract and Exclude are for unions; Pick and Omit are for object types.
If you are filtering members of a union type, you want
ExtractorExclude. If you are filtering keys of an object type, you wantPickorOmit.
The utility types are a small library of building blocks. You do not need all of them — but knowing which half-dozen fit your patterns is the difference between types that compose cleanly and types you fight with.