Introduction
TypeScript ships with a set of built-in utility types that perform common type transformations. These types are not magic — they are implemented using mapped types and conditional types that you have already learned. Understanding them deeply lets you use them correctly and compose them for complex transformations.
Key Concepts
Partial<T>: Makes all properties ofToptional.Required<T>: Makes all properties ofTrequired (removes?).Readonly<T>: Makes all properties ofTread-only.Pick<T, K>: Creates a type with only the specified keysKfromT.Omit<T, K>: Creates a type with all keys fromTexcept those inK.Record<K, V>: Creates an object type with keys of typeKand values of typeV.NonNullable<T>: Removesnullandundefinedfrom a union.
Real World Context
These utility types appear in virtually every TypeScript codebase. Partial<T> is used for update operations (PATCH endpoints), Pick<T, K> for selecting fields in API responses, Readonly<T> for immutable state, and Record<K, V> for lookup tables. Knowing which utility to reach for saves you from writing custom mapped types for common patterns.
Deep Dive
Partial<T>
Partial makes every property optional. Under the hood, it is a mapped type:
typescript// Built-in implementation: type Partial<T> = { [P in keyof T]?: T[P] }; // Usage: interface User { id: number; name: string; email: string; } type UpdateUser = Partial<User>; // { id?: number; name?: string; email?: string } function updateUser(id: number, fields: Partial<User>) { // fields.name is string | undefined } updateUser(1, { name: "Alice" }); // Only update name
This is the standard pattern for PATCH-style update functions where you only provide the fields you want to change.
Required<T>
Required is the opposite of Partial — it removes the ? modifier from all properties:
typescript// Built-in implementation: type Required<T> = { [P in keyof T]-?: T[P] }; interface Config { host?: string; port?: number; debug?: boolean; } type ResolvedConfig = Required<Config>; // { host: string; port: number; debug: boolean }
The -? modifier removes optionality. This is useful when you have a config type with defaults and want to represent the fully resolved config after defaults are applied.
Readonly<T>
Readonly adds the readonly modifier to every property:
typescript// Built-in implementation: type Readonly<T> = { readonly [P in keyof T]: T[P] }; interface State { count: number; items: string[]; } type FrozenState = Readonly<State>; const state: FrozenState = { count: 0, items: [] }; state.count = 1; // Error: Cannot assign to 'count' because it is a read-only property state.items.push("a"); // Warning: this still works! Readonly is shallow.
Important: Readonly is shallow. It prevents reassignment of properties but does not freeze nested objects or arrays.
Pick<T, K> and Omit<T, K>
Pick selects a subset of properties; Omit removes properties:
typescriptinterface User { id: number; name: string; email: string; password: string; } type PublicUser = Pick<User, "id" | "name" | "email">; // { id: number; name: string; email: string } type PublicUser2 = Omit<User, "password">; // { id: number; name: string; email: string }
Both approaches produce the same result here. Use Pick when you want to explicitly list what to include; use Omit when it is easier to list what to exclude.
Record<K, V>
Record creates an object type from a key type and value type:
typescripttype PageInfo = { title: string; url: string }; type Page = "home" | "about" | "contact"; const nav: Record<Page, PageInfo> = { home: { title: "Home", url: "/" }, about: { title: "About", url: "/about" }, contact: { title: "Contact", url: "/contact" }, };
This is useful for lookup tables, dictionaries, and configuration maps where you want every key in a union to have an entry.
NonNullable<T>
NonNullable removes null and undefined from a union type:
typescripttype MaybeString = string | null | undefined; type DefiniteString = NonNullable<MaybeString>; // string
Under the hood, it is implemented as a distributive conditional type: T extends null | undefined ? never : T.
Common Pitfalls
- Assuming
Readonlyis deep —Readonly<T>only makes the top-level properties read-only. Nested objects can still be mutated. For deep immutability, you need a customDeepReadonlytype or a library like Immer. - Using
Omitwith string literals not in the type —Omit<User, "nonexistent">compiles without error but does nothing. TypeScript does not warn you about omitting keys that do not exist.
Best Practices
- Compose utility types —
Readonly<Pick<User, "id" | "name">>creates a read-only view of selected properties. Chain them for precise transformations. - Use
satisfieswithRecordfor exhaustive checking —const x = {...} satisfies Record<Page, PageInfo>ensures every key is present while preserving literal types.
Summary
Partial<T>makes all properties optional;Required<T>makes them required.Readonly<T>prevents property reassignment (shallow only).Pick<T, K>selects properties;Omit<T, K>removes them.Record<K, V>creates a typed dictionary from a key union and value type.NonNullable<T>stripsnullandundefinedfrom unions.- These utility types compose together for complex transformations.
Code Examples
// Composing utility types for a real-world API pattern
interface User {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
}
// API response: no password, all fields required and read-only
type UserResponse = Readonly<Omit<User, "password">>;
// Update payload: no id/createdAt, all remaining fields optional
type UpdatePayload = Partial<Omit<User, "id" | "createdAt" | "password">>;
// Create payload: no id/createdAt, password required
type CreatePayload = Omit<User, "id" | "createdAt">;
function updateUser(id: number, data: UpdatePayload): UserResponse {
// data.name is string | undefined
// data.email is string | undefined
throw "implement";
}