The Utility Types You Actually Need
An order type existed in four hand-copied versions: the full record, the list item, the create payload and the update payload. A giftMessage field was added to two of them. The list view quietly dropped it, and support could not see gift messages until a customer asked why theirs was missing.
TypeScript ships with more than twenty utility types, and a handful of them cover nearly every real need. Below are the nine I reach for, with the cases where they help and the traps, such as Partial on a whole entity making its id optional.
- You reach for
Partial<T>for update payloads and end up with everything optional includingid - You've typed the same DTO three times by hand when
Pickwould have done it in one line - You're not sure when to use
ExtractvsPick— they sound the same but aren't - You use
Record<string, T>when you wantedRecord<SomeUnion, T>to force exhaustiveness
These nine utility types — and their compositions — eliminate nearly all manual type duplication in a typical TypeScript codebase.
The Core Nine
mindmap
root((Utility Types))
Object Shape
Partial
Required
Readonly
Pick
Omit
Union & Keys
Record
Extract
Exclude
Functions
ReturnType
Parameters
1. Partial and Required
interface User {
id: string;
name: string;
email: string;
avatar: string;
}
// Partial — every property becomes optional
type UpdateUserPayload = Partial<User>;
// { id?: string; name?: string; email?: string; avatar?: string }
// Problem: id shouldn't be updatable
// Solution: Partial on just the fields you want optional
type UpdateUserPayload = Partial<Omit<User, 'id'>> & { id: string };
// { id: string; name?: string; email?: string; avatar?: string }
// Required — reverse: make all optional properties required
interface Config {
timeout?: number;
retries?: number;
}
type ResolvedConfig = Required<Config>;
// { timeout: number; retries: number }
// Useful after you've applied defaults
function withDefaults(config: Config): ResolvedConfig {
return { timeout: 5000, retries: 3, ...config };
}2. Pick and Omit
Both create a new type from a subset of an object's properties. The difference is perspective: Pick lists what you want,Omitlists what you don't.
interface Article {
id: string;
title: string;
content: string;
authorId: string;
publishedAt: Date;
updatedAt: Date;
deletedAt: Date | null;
}
// Pick: ideal when you want a small, specific subset
type ArticleCard = Pick<Article, 'id' | 'title' | 'publishedAt'>;
// { id: string; title: string; publishedAt: Date }
// Omit: ideal when you want most of the type minus a few
type CreateArticlePayload = Omit<Article, 'id' | 'publishedAt' | 'updatedAt' | 'deletedAt'>;
// { title: string; content: string; authorId: string }
// Rule of thumb:
// Pick when you want 3 or fewer properties from a large type
// Omit when you want most of the type minus a few specific fields3. Record
Record<K, V> is not just shorthand for { [key: string]: V }. Its real power is with union key types that enforce exhaustiveness.
type Status = 'pending' | 'processing' | 'completed' | 'failed';
// ❌ Easy to miss a status
const labels: { [key: string]: string } = {
pending: 'Pending',
processing: 'Processing',
// forgot 'completed' and 'failed' — no error
};
// ✅ Record enforces every union member is handled
const labels: Record<Status, string> = {
pending: 'Pending',
processing: 'Processing',
completed: 'Completed',
// ❌ Error: property 'failed' is missing
failed: 'Failed',
};
// Also useful for caches and lookup tables
const userCache: Record<string, User> = {};
// With number keys (index signatures)
const httpMessages: Record<number, string> = {
200: 'OK',
404: 'Not Found',
500: 'Internal Server Error',
};4. Extract and Exclude
These operate on union types — not object types. That's the key difference from Pick and Omit.
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
// Extract: keep only members assignable to the second argument
type SafeMethods = Extract<HttpMethod, 'GET' | 'HEAD' | 'OPTIONS'>;
// 'GET' | 'HEAD' | 'OPTIONS'
// Exclude: remove members assignable to the second argument
type MutatingMethods = Exclude<HttpMethod, 'GET' | 'HEAD' | 'OPTIONS'>;
// 'POST' | 'PUT' | 'PATCH' | 'DELETE'
// Practical use: filtering a discriminated union
type Event =
| { type: 'click'; x: number; y: number }
| { type: 'keydown'; key: string }
| { type: 'focus'; element: string }
| { type: 'blur'; element: string };
type FocusEvents = Extract<Event, { type: 'focus' | 'blur' }>;
// { type: 'focus'; element: string } | { type: 'blur'; element: string }
type MouseEvents = Extract<Event, { type: string; x: number }>;
// { type: 'click'; x: number; y: number }5. ReturnType and Parameters
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// ReturnType — extract what a function returns
type FetchUserReturn = ReturnType<typeof fetchUser>;
// Promise<User>
// Awaited + ReturnType — unwrap the Promise too
type ResolvedUser = Awaited<ReturnType<typeof fetchUser>>;
// User
// Parameters — extract the parameter tuple
type FetchUserParams = Parameters<typeof fetchUser>;
// [id: string]
type FirstParam = Parameters<typeof fetchUser>[0];
// string
// Practical use: wrapper functions that preserve the signature
function withLogging<T extends (...args: any[]) => any>(
fn: T
): (...args: Parameters<T>) => ReturnType<T> {
return (...args) => {
console.log('Calling', fn.name, args);
return fn(...args);
};
}Composition Patterns
Deep Partial (for nested updates)
// Built-in Partial only goes one level deep
// For nested structures, define your own:
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface Settings {
theme: { primary: string; secondary: string };
notifications: { email: boolean; push: boolean };
}
// With built-in Partial:
type PartialSettings = Partial<Settings>;
// { theme?: { primary: string; secondary: string }; ... }
// ❌ Inner object is NOT partial — you must provide all theme properties
// With DeepPartial:
type DeepPartialSettings = DeepPartial<Settings>;
// { theme?: { primary?: string; secondary?: string }; ... }
// ✅ Can update just one nested fieldPick from a union to get the discriminant
type Action =
| { type: 'increment'; amount: number }
| { type: 'decrement'; amount: number }
| { type: 'reset' };
// Get just the discriminant values as a union
type ActionType = Action['type'];
// 'increment' | 'decrement' | 'reset'
// Map over them for a handler registry
type ActionHandlers = {
[K in ActionType]: (action: Extract<Action, { type: K }>) => void;
};One Source of Truth for the Order Story
Back to the four copies of the order type from the introduction. The fix is to write the full shape once and derive the rest, so a new field is either picked up everywhere or rejected by the compiler everywhere:
interface Order {
id: string;
customerId: string;
items: OrderItem[];
giftMessage?: string;
status: 'pending' | 'paid' | 'shipped';
createdAt: Date;
}
// Server-owned fields never come from the client
type CreateOrder = Omit<Order, 'id' | 'status' | 'createdAt'>;
// Updates: id is required, a whitelist of fields may change
type UpdateOrder = Pick<Order, 'id'> & Partial<Pick<Order, 'giftMessage' | 'items'>>;
// The list view shows a subset, chosen explicitly
type OrderListItem = Pick<Order, 'id' | 'status' | 'createdAt' | 'giftMessage'>;Note the difference between Omit and Pick here. CreateOrder uses Omit, so a new field on Order automatically becomes part of the create payload, which is usually what you want for input. OrderListItemuses Pick, so a new field stays out of the list until someone decides to add it. Choose per type based on which default is safer when the base changes, and the gift-message bug becomes a deliberate decision instead of an accident.
Omit on a Union Does Not Do What You Expect
This one catches experienced developers. Omit is built from keyof, and keyof on a union gives you only the keys every member shares. So omitting a field from a discriminated union quietly flattens it:
type Payment =
| { kind: 'card'; last4: string; auditId: string }
| { kind: 'bank'; iban: string; auditId: string };
type Bad = Omit<Payment, 'auditId'>;
// { kind: 'card' | 'bank' } last4 and iban are gone, narrowing no longer works
// Apply Omit to each member instead
type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
type Good = DistributiveOmit<Payment, 'auditId'>;
// { kind: 'card'; last4: string } | { kind: 'bank'; iban: string }The T extends unknown ? ... : never wrapper makes the conditional type distribute over each member of the union. Keep aDistributiveOmit in your shared types file; any time you derive from a union, reach for it instead of Omit.
When Derived Types Hurt Readability
Stack enough utilities and hovering over a type showsPick<Omit<Order, "id">, "items"> & Partial<...> instead of the fields. Error messages get the same treatment. A small helper flattens the result for display without changing the type:
type Prettify<T> = { [K in keyof T]: T[K] } & {};
type UpdateOrderView = Prettify<UpdateOrder>;
// hover: { id: string; giftMessage?: string; items?: OrderItem[] }Derive types internally, where staying in sync matters most. At public boundaries, such as an SDK you publish or an API contract other teams read, consider writing the interface out in full and adding a compile-time check that it still matches the derived version. The explicit interface is what readers see; the check keeps it honest.
Two More Worth Knowing: NoInfer and Your Own
NoInfer<T> (TypeScript 5.4) tells the compiler not to use a particular argument when working out a type parameter. Without it, a wrong value in a later argument widens the inferred type instead of producing an error.
function createState<T extends string>(states: T[], initial: NoInfer<T>) { /* ... */ }
createState(['idle', 'loading'], 'idle'); // ok
createState(['idle', 'loading'], 'done'); // error: 'done' is not 'idle' | 'loading'When no built-in type fits, compose your own from the same building blocks. Two that most codebases end up writing:
// Make only some keys optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
// Flatten an intersection so editor tooltips show the final shape
type Prettify<T> = { [K in keyof T]: T[K] } & {};
type NewUser = Prettify<PartialBy<User, 'id' | 'createdAt'>>;Keep helper types like these in one shared file with a comment and an example for each. A utility type nobody can find gets written again, slightly differently, in the next feature.
Pitfalls
Partial makes IDs optional too
The most common mistake: using Partial<T> for update payloads when id should remain required. Always combine with Omit when fields have different optionality requirements.
Record<string, T> accepts any key
When you have a known set of keys, use a union type instead of string. Record<Status, string> catches missing entries at compile time; Record<string, string>doesn't.
Extract vs Pick confusion
Pick operates on object types (removes properties). Extract operates on union types (filters union members). Using the wrong one produces an error or never — not a subtle type difference.

