Putting built-in type transformations to work
TypeScript ships Partial, Pick, Omit, Record, and several other utility types as ready-made building blocks for type-safe data structures, so developers never have to manually duplicate or keep existing interfaces in sync. Applying these built-in type transformations deliberately cuts redundancy across API contracts, forms, and build scripts, and lets the compiler catch inconsistencies early, long before they surface in production.
Table of Contents
- 1. Why utility types change everyday TypeScript work
- 2. Partial and Required: controlling optionality deliberately
- 3. Readonly: immutability at the type level
- 4. Pick and Omit: extracting subsets of a type
- 5. Record: type-safe key-value structures
- 6. Exclude and Extract: filtering union types precisely
- 7. NonNullable: reliably ruling out null and undefined
- 8. Combining utility types: nested patterns
- 9. Utility types compared side by side
- 10. Summary
- 11. FAQ
1. Why utility types change everyday TypeScript work
Without utility types, many TypeScript projects quickly end up with manually duplicated code: a Product interface gets copied for an update form, every field is hand-marked optional, and every change to the original has to be manually replayed onto the copy. This is exactly the problem the built-in utility types from lib.es5.d.ts solve: they transform an existing type into a new one at compile time, so no second source of truth ever appears. The compiler derives the resulting type automatically from the original.
Technically, most utility types are so-called mapped types, generic types that walk every property of an input type via keyof and indexed access, then emit a transformed version. This happens entirely at compile time; no extra code exists at runtime, and bundle size is never affected. For teams that reuse the same domain models across REST clients, GraphQL resolvers, build scripts, and frontend forms, utility types are therefore not a nice-to-have but the foundation of consistent type contracts across the whole project.
2. Partial and Required: controlling optionality deliberately
Partial<T> makes every property of a type optional and is the standard choice for update or patch functions: a caller should only pass the fields that actually change, without having to resend the entire object state. Without Partial, a team would either have to maintain a fully separate update interface or fall back to any, losing type safety exactly where it matters most: at the point of writing data.
Required<T> is the exact inverse and strips every optional modifier. This is useful when a draft type with many optional fields needs to be guaranteed complete after merging with defaults, the compiler then enforces that every field really has been filled before the value is used further. Both utility types are shallow: nested objects inside property values are not transformed recursively, which matters for deeply nested domain models.
interface Product {
sku: string;
name: string;
price: number;
description: string;
stockQty: number;
active: boolean;
}
declare function getProductBySku(sku: string): Product;
// Partial<T>: every field becomes optional - ideal for PATCH-style updates
function updateProduct(sku: string, changes: Partial<Product>): Product {
const existing = getProductBySku(sku);
return { ...existing, ...changes };
}
updateProduct('MS-1001', { price: 59.9 }); // only the price field changes
// Required<T>: every field becomes mandatory - useful after merging defaults
interface ProductDraft {
sku: string;
name?: string;
price?: number;
}
const defaults: Required<ProductDraft> = {
sku: 'MS-0000',
name: 'Untitled product',
price: 0,
};
function finalizeProduct(draft: ProductDraft): Required<ProductDraft> {
return { ...defaults, ...draft };
}
3. Readonly: immutability at the type level
Readonly<T> marks every top-level property of a type as read-only. An assignment attempt like snapshot.total = 0 gets rejected by the compiler with a clear error before the code ever runs. That's especially valuable for function return values meant to act as an immutable snapshot, for example an order status that, once loaded, should never be accidentally mutated by calling code.
The key limitation is that Readonly is only shallow: an array or nested object inside the properties remains mutable, because the transformation doesn't walk recursively through the structure. True deep immutability requires a custom recursive mapped type along the lines of DeepReadonly<T>, which sends every property back through itself as long as it's still an object. Combined with Object.freeze() at runtime, this produces a type system that guards against accidental mutation both at compile time and while the program is running.
interface OrderItem {
sku: string;
qty: number;
}
interface Order {
id: string;
customerId: string;
total: number;
items: OrderItem[];
}
declare function loadOrder(id: string): Order;
// Readonly<T> is shallow: top-level props cannot be reassigned,
// but nested objects and arrays remain mutable.
function freezeOrderSnapshot(order: Order): Readonly<Order> {
return Object.freeze({ ...order });
}
const snapshot = freezeOrderSnapshot(loadOrder('ORD-42'));
// snapshot.total = 0; // Error: read-only property
snapshot.items.push({ sku: 'X', qty: 1 }); // allowed - items array is not frozen
// DeepReadonly via a recursive mapped type, for true nested immutability
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
const deepSnapshot: DeepReadonly<Order> = freezeOrderSnapshot(loadOrder('ORD-42'));
// deepSnapshot.items.push(...) // Error: items is readonly too
4. Pick and Omit: extracting subsets of a type
Pick<T, K> produces a new type containing only the keys named in K, typically a set of string literals joined with |. It's the tool of choice when a function or component deliberately needs only a small slice of a larger domain model, for example a public customer profile that shows only a name and ID, never internal fields like a password hash.
Omit<T, K> solves the inverse problem: every field is kept except the ones explicitly named. For API responses this is often the better choice, since it avoids listing every single public field, keeping only the few that must never leak, a pattern that's more resilient to future, harmless extensions of the source type. Internally, Omit is itself nothing more than a combination of Pick and Exclude, showing just how consistently the utility types build on each other.
interface Customer {
id: string;
email: string;
firstName: string;
lastName: string;
passwordHash: string;
newsletterOptIn: boolean;
}
// Pick<T, K>: select only the keys the public profile actually needs
type PublicProfile = Pick<Customer, 'id' | 'firstName' | 'lastName'>;
function toPublicProfile(customer: Customer): PublicProfile {
const { id, firstName, lastName } = customer;
return { id, firstName, lastName };
}
// Omit<T, K>: keep everything except the field that must never leave the server
type CustomerApiResponse = Omit<Customer, 'passwordHash'>;
function toApiResponse(customer: Customer): CustomerApiResponse {
const { passwordHash, ...rest } = customer;
return rest;
}
// Omit is itself built from Pick and Exclude:
// type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
5. Record: type-safe key-value structures
Record<K, V> constructs an object type whose keys come from K and whose values are all of type V. The decisive advantage over a generic index signature like { [key: string]: number } shows up as soon as K is a union of string literals: the compiler then enforces that every possible key in the union is actually present. Forget a customer group when building a Record<CustomerGroup, number> literal, and TypeScript reports the error immediately instead of failing silently at runtime when the missing key is accessed.
This exhaustiveness check makes Record the ideal tool for discount tiers, translation tables, or status maps where every member of a union must be explicitly handled. Extend the underlying union later with a new value, say an additional customer group, and the compiler automatically flags every spot in the code where a Record literal is now incomplete, a safety net that plain JavaScript objects or loosely typed maps simply don't offer.
type CustomerGroup = 'retail' | 'wholesale' | 'vip';
// Record<K, V>: exhaustive, type-checked map - a missing key is a compile error
const groupDiscounts: Record<CustomerGroup, number> = {
retail: 0,
wholesale: 0.15,
vip: 0.25,
};
interface Product {
sku: string;
name: string;
categoryCode: string;
}
// Record<string, T[]> for dynamic, non-exhaustive grouping keys
function groupProductsByCategory(products: Product[]): Record<string, Product[]> {
return products.reduce<Record<string, Product[]>>((acc, product) => {
const category = product.categoryCode ?? 'uncategorized';
(acc[category] ??= []).push(product);
return acc;
}, {});
}
// Adding a new CustomerGroup member later forces every Record<CustomerGroup, ...>
// literal in the codebase to be updated - the compiler will not let it slide.
6. Exclude and Extract: filtering union types precisely
Exclude<T, U> and Extract<T, U> work differently from Pick and Omit: they don't operate on the keys of an object type, they operate on the members of a union type. Exclude removes every member of T that's assignable to U; Extract keeps exactly those members and discards the rest. The two mirror each other much like Pick and Omit do at the object level, just one level deeper, at the level of individual type values within a union.
In practice, Exclude and Extract are excellent for deriving targeted subsets from a broad status union such as an order status: a group of closed states for reporting purposes, a group of open states for follow-up notifications. Rather than maintaining these subsets as separate, redundant string-literal unions, you derive them directly from the one canonical union, so if the source union changes, the change propagates automatically into every derived subset, and the compiler flags every spot that becomes inconsistent as a result.
7. NonNullable: reliably ruling out null and undefined
NonNullable<T> strips null and undefined from a type and matters most in projects with strictNullChecks enabled, where those two values are explicitly visible in the type system instead of implicitly allowed everywhere. A typical use case: an API response field is typed as Order | null | undefined because it's optional in the database, but after an explicit check in the code, the resulting type should be guaranteed to no longer include null or undefined, so the rest of the code doesn't need constant optional-chaining operators.
It's worth noting that NonNullable alone never substitutes for a runtime check, the type only describes what the compiler may assume after a check has already happened. The actual safeguard still comes from an if guard, a throw on a missing value, or an array filter with a custom type guard. NonNullable simply makes the return type of that check precise, instead of reaching for a blanket ! non-null assertion operator that bypasses any actual verification by the compiler.
8. Combining utility types: nested patterns
The real strength of the built-in utility types only shows up once they're combined. Partial<Pick<T, K>>, for instance, first selects a specific subset of fields and then makes exactly that subset optional, the perfect pattern for a patch DTO that's only ever allowed to touch a few, clearly named fields, all of them optional. A generic Partial<T> would be too permissive here, since it would accidentally allow sensitive fields like a SKU or an ID to be changed too.
A second common pattern is the intersection of Omit and Partial<Pick<...>>: Omit<T, K> & Partial<Pick<T, K>> describes a type where most fields stay mandatory just like in the original, but a clearly named subset becomes explicitly optional, for example a create input where the generated SKU is missing at creation time, while a description may optionally be supplied later. Once more than two utility types get nested, it pays off to extract a named intermediate type; otherwise readability suffers noticeably without any real gain in type safety.
interface Product {
sku: string;
name: string;
price: number;
description: string;
stockQty: number;
active: boolean;
}
// Combining utility types: only price, stockQty and active are editable, and optional
type ProductPatch = Partial<Pick<Product, 'price' | 'stockQty' | 'active'>>;
function patchProduct(sku: string, patch: ProductPatch): void {
// sku and name can never be touched through this function's signature
applyPatch(sku, patch);
}
declare function applyPatch(sku: string, patch: ProductPatch): void;
// Combining with an intersection: most fields required, one explicitly optional
type ProductCreateInput = Omit<Product, 'sku'> & Partial<Pick<Product, 'description'>>;
function createProduct(sku: string, input: ProductCreateInput): Product {
return { sku, description: '', ...input };
}
// Exclude/Extract narrow a union, NonNullable strips null/undefined after a guard
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'refunded';
type ClosedStatus = Extract<OrderStatus, 'delivered' | 'cancelled' | 'refunded'>;
type OpenStatus = Exclude<OrderStatus, ClosedStatus>;
interface CustomerWithOrder {
id: string;
lastOrder: Product | null | undefined;
}
function requireLastOrder(customer: CustomerWithOrder): NonNullable<CustomerWithOrder['lastOrder']> {
if (!customer.lastOrder) {
throw new Error(`Customer ${customer.id} has no orders yet`);
}
return customer.lastOrder;
}
9. Utility types compared side by side
The nine most important built-in utility types solve different problems and can be sorted by three questions: does the transformation act on object keys or on union members? Does it change optionality or mutability? And does it produce a subset or a structural remapping? The table below summarizes the practical use of each type.
| Utility Type | What it does | Example | When to use |
|---|---|---|---|
| Partial<T> | Makes every property optional | Partial<Product> |
Patch/update functions |
| Required<T> | Makes every property mandatory | Required<ProductDraft> |
Enforcing completeness after defaults |
| Readonly<T> | Top-level properties become read-only (shallow) | Readonly<Order> |
Immutable snapshots/return values |
| Pick<T, K> | Selects a subset of keys | Pick<Customer, 'id'|'email'> |
Public DTOs, projections |
| Omit<T, K> | Removes a subset of keys | Omit<Customer, 'passwordHash'> |
Stripping sensitive fields from responses |
| Record<K, V> | Exhaustive key-value structure | Record<CustomerGroup, number> |
Maps over a known union of keys |
| Exclude<T, U> | Removes union members | Exclude<Status, Closed> |
Deriving subsets from a status union |
| Extract<T, U> | Keeps matching union members | Extract<Status, Closed> |
Isolating a filtered subset of a union |
| NonNullable<T> | Removes null and undefined | NonNullable<Order | null> |
Sharpening a type after an existence check |
The table also shows where types combine: Pick and Partial pair up into patch DTOs, Exclude and Extract pair up into complete union decompositions, and Omit is ultimately just a predefined combination of Pick and Exclude. Understanding these building blocks means you rarely need to write your own mapped types from scratch.
Mironsoft
TypeScript tooling, type-safe build scripts, and headless integrations
Ready for type-safe data contracts instead of copy-pasted interfaces?
We build TypeScript type systems for frontend tooling, build scripts, and headless Magento integrations that consistently rely on utility types instead of duplicated interfaces, maintainable, redundancy-free, and with the compiler as the first line of defense.
Type system audit
Reviewing existing type definitions for redundancy and missing utility types
DTO refactoring
Moving API contracts and form models onto Pick/Omit/Partial
Build tooling
Type-safe build scripts and headless connections into Magento stores
10. Summary
TypeScript's built-in utility types solve a recurring problem: domain models need slightly different shapes in different contexts, without spawning multiple manually maintained interfaces. Partial and Required control optionality, Readonly guards against accidental mutation at compile time, Pick and Omit extract and remove subsets of fields respectively, Record enforces complete key-value coverage, Exclude and Extract filter union types, and NonNullable sharpens a type after an existence check.
The biggest leverage comes from combining these building blocks: Partial<Pick<T, K>> and Omit<T, K> & Partial<Pick<T, K>> cover most patch and create DTO scenarios without a single additional interface written by hand. Teams that consistently reach for these patterns instead of scattered any workarounds end up with a type system that grows alongside the domain model, instead of having to be maintained in several places at once with every change.
Utility Types Overview - The Essentials at a Glance
Partial & Required
Control optionality for patch objects and fully validated configs, without duplicating the original interface.
Readonly
Guards top-level fields against mutation at compile time, but only shallowly, use DeepReadonly for true nested immutability.
Pick & Omit
Extract and remove fields respectively, for DTOs, public profiles, and API responses.
Record, Exclude, Extract, NonNullable
Build exhaustive key-value maps, filter union types precisely, and strip null/undefined after checks.