You've been writing TypeScript. But are you writing it well — or are you manually duplicating types that the compiler could derive for you in one line?
One of TypeScript's least-celebrated superpowers is its standard library of utility types — generic helpers built into the language that let you transform, compose, and derive new types from existing ones without duplicating a single definition.
Most developers know Partial and Readonly. Fewer reach for ReturnType, Parameters, or Awaited when they should. And almost nobody uses infer directly until they've felt the pain of not having it.
This article is a practical guide to the utility types that should become muscle memory. Each one is explained with the problem it solves, not just what it does — because knowing the syntax is easy. Knowing when to reach for it is what separates TypeScript that ages well from TypeScript that becomes a maintenance nightmare.
The Foundational Four
These four utility types appear in almost every non-trivial TypeScript codebase. If you're not using them, you're probably writing duplicate types.
Partial<T>
Makes all properties of T optional.
The problem it solves: You have a fully defined type for a domain entity, but you need a version for update payloads where the caller only provides the fields they want to change.
interface User {
id: string;
name: string;
email: string;
role: "admin" | "member" | "viewer";
}
// ❌ Don't do this — duplicated definition, drift risk
interface UpdateUserPayload {
name?: string;
email?: string;
role?: "admin" | "member" | "viewer";
}
// ✅ Do this
type UpdateUserPayload = Partial<User>;
function updateUser(id: string, payload: Partial<User>): Promise<User> {
// ...
}
Watch out: Partial only shallowly makes properties optional. Nested objects remain as-is. For deep partiality, you'll need a recursive utility type (more on that later).
Required<T>
Makes all properties of T required, stripping any ? modifiers.
The problem it solves: Configuration objects with sensible defaults — you accept a Partial config from the user, merge it with defaults, and then want to guarantee downstream functions receive a fully resolved config.
Makes all properties of T read-only. Assignments to any property become a compile-time error.
The problem it solves: Expressing immutability intent at the type level. Particularly useful for Redux state, configuration objects, and data returned from API calls that shouldn't be mutated.
function getConfig(): Readonly<Config> {
return { timeout: 5000, retries: 3 };
}
const config = getConfig();
config.timeout = 10000; // ❌ Error: Cannot assign to 'timeout' because it is a read-only property
Pro tip: Combine with as const for deeply immutable object literals. Readonly is shallow; as const is deep.
const ROUTES = {
home: "/",
dashboard: "/dashboard",
settings: "/settings",
} as const;
type Route = (typeof ROUTES)[keyof typeof ROUTES];
// type Route = "/" | "/dashboard" | "/settings"
Pick<T, K>
Constructs a type by picking only the specified keys from T.
The problem it solves: Creating focused view types or API response shapes that only expose a subset of a larger model — without manually redefining those properties.
interface User {
id: string;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
lastLoginAt: Date;
}
// ❌ Never expose this to the client
// type PublicUser = User;
// ✅ Expose only what the client needs
type PublicUser = Pick<User, "id" | "name" | "email">;
// Also great for component props derived from a larger model
type UserCardProps = Pick<User, "id" | "name"> & {
onSelect: (id: string) => void;
};
The Counterparts
Every transformation type has a complement. Knowing both lets you express intent precisely rather than always constructing from zero.
Omit<T, K>
Constructs a type by removing the specified keys from T. The inverse of Pick.
When to prefer Omit over Pick: When you want most of a type except a few fields. If you're picking 10 keys out of 12, use Omit with the 2 you're removing.
// ✅ Much more readable than Pick<User, "name" | "email" | "createdAt" | ...>
type UserWithoutSensitiveData = Omit<User, "passwordHash">;
// Common pattern: omit auto-generated fields for create payloads
type CreateUserPayload = Omit<User, "id" | "createdAt" | "lastLoginAt">;
Exclude<UnionType, ExcludedMembers>
Constructs a type by removing members from a union type. Omit works on object keys; Exclude works on union members.
type Status = "pending" | "active" | "suspended" | "deleted";
type ActiveStatus = Exclude<Status, "deleted" | "suspended">;
// type ActiveStatus = "pending" | "active"
// Extremely useful for discriminated unions
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
type NonCircle = Exclude<Shape, { kind: "circle" }>;
// Removes the circle variant from the union
Extract<Type, Union>
The complement of Exclude. Keeps only the union members assignable to Union.
type StringOrNumber = string | number | boolean | null;
type OnlyPrimitives = Extract<StringOrNumber, string | number>;
// type OnlyPrimitives = string | number
// Practical: extracting specific event types
type MouseEvents = Extract<Event, MouseEvent | PointerEvent>;
The Function Introspectors
These are where TypeScript gets genuinely powerful. Instead of manually declaring types that describe your functions, you derive them from the functions themselves. This means your types automatically stay in sync as implementations change.
ReturnType<T>
Extracts the return type of a function type T.
The problem it solves: You have a function whose return type is complex or inferred — and you need that type elsewhere without manually redeclaring it.
// A function with a complex, inferred return type
function createUserSession(userId: string) {
return {
sessionId: crypto.randomUUID(),
userId,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 3600 * 1000),
metadata: { ipAddress: "", userAgent: "" },
};
}
// ✅ Derive the type from the source of truth
type UserSession = ReturnType<typeof createUserSession>;
// Now UserSession is always in sync — no manual maintenance
function invalidateSession(session: UserSession): void {
// ...
}
This pattern is especially valuable with factory functions, ORM query builders, and anything that returns a structurally complex object.
Parameters<T>
Extracts the parameter types of a function type T as a tuple.
The problem it solves: You want to type-safely pass around or wrap function arguments without redeclaring the parameter types.
Recursively unwraps Promise<T> to get the resolved type. This is essential in modern async TypeScript.
async function fetchUser(id: string) {
const response = await fetch(`/api/users/${id}`);
return response.json() as Promise<User>;
}
// ✅ Get the resolved type without calling the function
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;
// type FetchedUser = User
// Also handles nested promises
type DeepResolved = Awaited<Promise<Promise<Promise<string>>>>;
// type DeepResolved = string
ConstructorParameters<T>
Like Parameters, but for class constructors.
class HttpClient {
constructor(
private baseUrl: string,
private timeout: number,
private headers: Record<string, string>
) {}
}
type HttpClientArgs = ConstructorParameters<typeof HttpClient>;
// type HttpClientArgs = [string, number, Record<string, string>]
// Useful for dependency injection containers and factory patterns
function createHttpClient(...args: ConstructorParameters<typeof HttpClient>) {
return new HttpClient(...args);
}
The Record and Non-Nullable Essentials
Record<Keys, Type>
Constructs an object type whose keys are Keys and values are Type. Cleaner and more expressive than an index signature for finite key sets.
type Status = "pending" | "active" | "suspended";
// ❌ Index signature — too broad, allows any string key
type StatusLabels = { [key: string]: string };
// ✅ Record — exhaustive, only allows valid status keys
const STATUS_LABELS: Record<Status, string> = {
pending: "Awaiting Review",
active: "Active",
suspended: "Account Suspended",
// ❌ TypeScript will error if you add an unknown status
// ❌ TypeScript will error if you forget a status
};
// Combining with other utilities
type UsersByStatus = Record<Status, User[]>;
Why it matters: Record<Status, string> is exhaustive. If you add a new member to the Status union, TypeScript will immediately flag every Record<Status, ...> that doesn't include the new key. That's a safety net you don't get with a generic index signature.
NonNullable<T>
Removes null and undefined from a type.
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>;
// type DefiniteUser = User
// Extremely useful for narrowing after null checks
function processUser(user: User | null) {
if (!user) return;
// TypeScript narrows automatically here, but NonNullable is great
// for derived types in generic contexts
type ProcessedUser = NonNullable<typeof user>;
}
// Practical with mapped types and conditional filtering
type FilterNullable<T> = {
[K in keyof T]: NonNullable<T[K]>;
};
Combining Utility Types: Real-World Patterns
The real power emerges when you compose utility types together. Here are patterns that appear constantly in production codebases.
The Create/Update Pattern
interface Product {
id: string; // auto-generated
slug: string; // auto-generated from name
name: string;
price: number;
description: string;
tags: string[];
createdAt: Date; // auto-generated
updatedAt: Date; // auto-generated
}
// For creation: require user-provided fields, omit auto-generated ones
type CreateProductPayload = Omit<Product, "id" | "slug" | "createdAt" | "updatedAt">;
// For updates: everything optional except the identifier
type UpdateProductPayload = Partial<Omit<Product, "id" | "slug" | "createdAt">>;
// For search results: public-facing subset
type ProductSummary = Pick<Product, "id" | "slug" | "name" | "price">;
The Configuration Pattern
interface ServerConfig {
host: string;
port: number;
ssl: boolean;
maxConnections: number;
timeout: number;
logLevel: "debug" | "info" | "warn" | "error";
}
// User provides partial config; system merges with defaults
type UserConfig = Partial<ServerConfig>;
// Once merged, the config is fully resolved and immutable
type ResolvedConfig = Readonly<Required<ServerConfig>>;
function resolveConfig(userConfig: UserConfig): ResolvedConfig {
return Object.freeze({ ...DEFAULTS, ...userConfig });
}
The Event Handler Pattern
// Derive event handler types from the events they handle
type EventMap = {
click: MouseEvent;
keydown: KeyboardEvent;
submit: SubmitEvent;
resize: UIEvent;
};
type EventHandler<K extends keyof EventMap> = (event: EventMap[K]) => void;
type ClickHandler = EventHandler<"click">;
// type ClickHandler = (event: MouseEvent) => void
// Build a fully typed event emitter
type EventListeners = {
[K in keyof EventMap]?: EventHandler<K>[];
};
Deep Utility Types: When the Built-Ins Aren't Enough
The standard utility types are shallow. For recursive transformations, you need custom recursive types — but they follow the same patterns.
DeepPartial<T>
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
interface AppState {
user: {
profile: { name: string; avatar: string };
preferences: { theme: "light" | "dark"; language: string };
};
ui: { sidebarOpen: boolean; activeModal: string | null };
}
// Partial only affects the top level — nested objects remain required
type ShallowPartialState = Partial<AppState>;
// DeepPartial makes everything optional at every depth
type DeepPartialState = DeepPartial<AppState>;
DeepReadonly<T>
type DeepReadonly<T> = T extends (infer U)[]
? ReadonlyArray<DeepReadonly<U>>
: T extends object
? { readonly [P in keyof T]: DeepReadonly<T[P]> }
: T;
type ImmutableConfig = DeepReadonly<AppState>;
// Every nested property is now readonly, including arrays
Quick Reference
Utility Type
What It Does
When to Use It
Partial<T>
All props optional
Update payloads, optional config
Required<T>
All props required
After merging with defaults
Readonly<T>
All props read-only
State, config, API responses
Pick<T, K>
Keep only K keys
View models, focused props
Omit<T, K>
Remove K keys
Remove sensitive/auto fields
Exclude<U, E>
Remove union members
Filtering discriminated unions
Extract<T, U>
Keep union members
Narrowing union to a subset
Record<K, V>
Object with key type K
Exhaustive maps, lookup tables
NonNullable<T>
Remove null/undefined
Post-null-check types
ReturnType<T>
Function return type
Derive types from functions
Parameters<T>
Function param types
Wrappers, middleware, forwarding
Awaited<T>
Unwrap Promise
Async return types
ConstructorParameters<T>
Constructor param types
Factories, DI containers
The Mindset Shift
Utility types aren't just convenient shortcuts. They represent a fundamentally different way of thinking about types in TypeScript:
Types as transformations, not declarations.
Instead of asking "what shape does this data have?" ask "what is this type in relation to other types I already have?" A CreatePayload isn't a new type — it's your canonical Entity type with some keys omitted and some made optional. A PublicView isn't a new type — it's your full model with sensitive keys picked away.
When you think in transformations, your type definitions become a graph with a clear source of truth at each domain entity. Change the source, and the derived types update automatically. You can't drift. You can't forget to update a CreatePayload when you add a required field to Entity.
That's the real promise of utility types: not less typing, but less wrong typing. And in TypeScript, that's the whole game.