TypeScript Utility Types: The 12 That Do Real Work
Partial, Pick, Omit, Record and the rest — what each one is for, when to reach for it, and the composition patterns that replace hand-written duplicate types.
Table of contents
- The everyday four
- Record builds map types
- Narrowing unions
- Extracting from functions and promises
- Composition is where the value is
- Frequently asked questions
- Should I write my own utility types?
- Why does Partial not work recursively?
- What is the difference between Omit and Exclude?
- Are utility types free at runtime?
- Related reading
- References
TypeScript ships utility types that derive new types from existing ones. Using them well means your types stay in sync automatically — the alternative is hand-written duplicates that silently drift when a field is renamed.
The everyday four#
type User = {
id: string;
email: string;
name: string;
createdAt: Date;
};
// All properties optional — the shape of a PATCH body
type UserUpdate = Partial<User>;
// All properties required, stripping any ?
type CompleteUser = Required<PartialUser>;
// Choose properties to keep
type UserSummary = Pick<User, 'id' | 'name'>;
// Choose properties to remove
type PublicUser = Omit<User, 'email'>;Pick and Omit are the workhorses. Prefer Omit when the type will gain fields you want included by default, and Pick when it should stay deliberately narrow — a Pick will not silently start exposing a new field someone adds to User.
Record builds map types#
type Role = 'admin' | 'editor' | 'viewer';
type Permissions = Record<Role, string[]>;
// { admin: string[]; editor: string[]; viewer: string[] }The valuable property here is exhaustiveness: add 'owner' to Role and every Permissions object becomes a compile error until you handle it. That turns a runtime "undefined permissions" bug into a build failure.
Narrowing unions#
type Status = 'idle' | 'loading' | 'success' | 'error';
type Settled = Exclude<Status, 'idle' | 'loading'>; // 'success' | 'error'
type Pending = Extract<Status, 'idle' | 'loading'>; // 'idle' | 'loading'
type Defined = NonNullable<string | null | undefined>; // stringNonNullable is the one you reach for most, usually after a filter:
const ids: Array<string | null> = ['a', null, 'b'];
const clean = ids.filter((id): id is NonNullable<typeof id> => id !== null);
// string[]Extracting from functions and promises#
async function fetchUser(id: string): Promise<User> {
/* ... */
}
type Args = Parameters<typeof fetchUser>; // [id: string]
type Wrapped = ReturnType<typeof fetchUser>; // Promise<User>
type Value = Awaited<ReturnType<typeof fetchUser>>; // UserThis is the pattern that keeps a wrapper in sync with what it wraps:
function withLogging<F extends (...args: never[]) => unknown>(fn: F) {
return (...args: Parameters<F>): ReturnType<F> => {
console.log('calling', fn.name);
return fn(...args) as ReturnType<F>;
};
}Awaited recursively unwraps nested promises, which ReturnType alone does not.
Composition is where the value is#
Utility types compose, and that is how you express real-world shapes without repetition:
// Everything optional except the id — a typical update payload
type UpdatePayload = Partial<Omit<User, 'id'>> & Pick<User, 'id'>;
// The API response shape, derived rather than declared
type ApiUser = Omit<User, 'createdAt'> & { createdAt: string };That last one encodes a real fact: JSON has no Date, so a serialised user has a string. Deriving it means renaming a field on User updates both.
Frequently asked questions#
Should I write my own utility types?#
Sometimes. DeepPartial<T> and Prettify<T> (which flattens an intersection so hover output is readable) are worth having. Anything more elaborate usually signals that the underlying type should be restructured.
Why does Partial not work recursively?#
Because it maps one level only. Nested objects stay required. That is deliberate — recursion would be surprising — so a recursive version has to be written explicitly.
What is the difference between Omit and Exclude?#
Omit removes properties from an object type. Exclude removes members from a union. Different jobs, similar names.
Are utility types free at runtime?#
Entirely. They are erased at compile time and emit no JavaScript.
Related reading#
- Type vs Interface in TypeScript — which to use as your base type
- TypeScript Generics — the mechanism these are built on