TypeScript's Built-In Utility Types:From Mapped Types to Everyday Patterns
Most built-in utility types are compact mapped-type or conditional-type transformations, and understanding that makes them easier to compose safely in real projects.
When people first learn TypeScript utility types, they usually memorize a list: Partial, Required, Readonly, Pick, Omit, and Record. That works for day one, but it misses the deeper idea that makes these tools predictable.
Most built-in utility types are small abstractions built from mapped types and conditional types. Once you understand that, utilities stop feeling magical. You can predict behavior, compose patterns confidently, and create your own types when the built-ins are close but not exact.
Why mapped types matter
A mapped type transforms an existing object type by iterating over keys from keyof. At its simplest, it can rebuild a type without changes, but the same syntax lets you add modifiers, remove modifiers, and reshape value slots while preserving key structure.
type Transform<T> = {
[K in keyof T]: T[K];
};
From there, the jump to built-in utilities is small: add ?, add readonly, or filter which keys survive the transformation.
The core trio: Partial, Required, Readonly
These are the cleanest mapped-type examples because the transformation is mechanical and easy to inspect.
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
type User = {
id: string;
name: string;
email?: string;
};
type UserDraft = Partial<User>;
type UserStrict = Required<User>;
type UserView = Readonly<User>;
In practice, Partial fits draft objects and patch payloads, Required fits validation boundaries, and Readonly helps prevent accidental reassignment in state-like structures.
Pick and Omit for model slicing
Pick<T, K> keeps selected keys, while Omit<T, K> removes selected keys. Together, they let one canonical domain model generate multiple view shapes without copy-pasting properties.
type Product = {
id: string;
name: string;
price: number;
cost: number;
createdAt: string;
updatedAt: string;
};
type ProductPublic = Pick<Product, 'id' | 'name' | 'price'>;
type ProductInternal = Omit<Product, 'cost'>;
Conceptually, Omit is usually described as Pick plus key filtering via Exclude.
type MyOmit<T, K extends keyof any> =
Pick<T, Exclude<keyof T, K>>;
Record for exhaustive object maps
Record<K, V> is effectively a mapped type where keys come from a union instead of an existing object. It is excellent for lookups and configuration maps that should be exhaustive.
type Environment = 'development' | 'staging' | 'production';
const apiBaseByEnv: Record<Environment, string> = {
development: 'http://localhost:3000',
staging: 'https://staging.example.com',
production: 'https://example.com',
};
If one key is missing, TypeScript reports it at compile time. That makes Record a practical exhaustiveness tool, not just a dictionary shortcut.
Conditional helpers that pair with mapped types
Some built-ins are conditional rather than mapped, but they frequently compose with mapped utilities: Exclude, Extract, and NonNullable.
type Role = 'admin' | 'editor' | 'viewer' | null;
type InternalRole = Exclude<Role, 'viewer' | null>;
type ViewerRole = Extract<Role, 'viewer' | 'guest'>;
type SafeRole = NonNullable<Role>;
Function and async utilities
Utility types also keep function contracts synchronized: Parameters, ReturnType, and Awaited are especially useful in service layers and API clients.
async function fetchArticle(slug: string) {
return {
slug,
minutesRead: 10,
title: 'Mapped Types in TypeScript',
};
}
type FetchArticleArgs = Parameters<typeof fetchArticle>;
type FetchArticleResult = ReturnType<typeof fetchArticle>;
type ArticleData = Awaited<FetchArticleResult>;
Composition in real models
Most teams get the strongest payoff by composing utility types instead of applying a single helper in isolation.
type Article = {
id: string;
slug: string;
title: string;
body: string;
createdAt: string;
updatedAt: string;
isPublished: boolean;
};
type ArticleCreateInput = Omit<Article, 'id' | 'createdAt' | 'updatedAt'>;
type ArticleUpdateInput = Partial<ArticleCreateInput>;
type ArticleListItem = Pick<Article, 'id' | 'slug' | 'title' | 'isPublished'>;
One custom utility to cement the idea
After you understand the built-ins, custom helpers become straightforward. Here is a mapped type that forces any key ending in Id to be required and non-null.
type RequireIdFields<T> = {
[K in keyof T]-?: K extends `${string}Id`
? NonNullable<T[K]>
: T[K];
};
type Payment = {
orderId?: string | null;
customerId?: string | null;
amount?: number;
};
type NormalizedPayment = RequireIdFields<Payment>;
Common mistakes to avoid
1 Assuming Readonly is deep
Readonly<T> protects only top-level properties unless you implement a deep variant.
2 Using Partial everywhere
A broad Partial can hide required workflow invariants that a union would model more clearly.
3 Over-broad Record keys
Prefer finite key unions instead of string when exhaustiveness matters.
Decision guide
| Need | Utility type | Typical use case |
|---|---|---|
| Optional draft | Partial | Patch payloads and in-progress forms |
| Mandatory boundary | Required | Validated entities before persistence |
| Immutable surface | Readonly | View models and defensive state contracts |
| Narrow shape | Pick or Omit | API DTO variants from one source model |
| Exhaustive key map | Record | Environment and feature configuration |
Bottom line
If utility types feel like a random toolbox, mapped types are the principle that makes the toolbox coherent. Once you see Partial, Readonly, Pick, Omit, and Record as repeatable transformations, type design becomes far more intentional.
That perspective is what turns utility types from memorized syntax into practical architecture for safer TypeScript code.