TypeScript Utility Types for JSON: Partial, Pick, Omit, Record, and More
Last updated:
TypeScript's built-in utility types transform existing types without duplicating them — essential for modeling JSON API request and response shapes. This guide covers Partial, Required, Pick, Omit, Record, Readonly, Extract, Exclude, and custom DeepPartial, with practical JSON API patterns.
1. Quick Reference Table
| Utility Type | What it does | JSON API use case |
|---|---|---|
Partial<T> | All fields optional | PATCH request body |
Required<T> | All fields required | Assert response is complete |
Readonly<T> | All fields read-only | Immutable API response |
Pick<T, K> | Keep only K keys | List response summary type |
Omit<T, K> | Remove K keys | Create/update request (remove id, timestamps) |
Record<K, V> | Object with K keys, V values | Lookup tables, grouped data |
Extract<T, U> | Keep union members matching U | Active status subset |
Exclude<T, U> | Remove union members matching U | Non-null, non-draft types |
NonNullable<T> | Remove null and undefined | Assert field is present after check |
ReturnType<T> | Infer function return type | Type fetch function response |
2. CRUD Type Modeling with One Base Interface
// Base entity — source of truth
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
bio: string | null
createdAt: string
updatedAt: string
}
// POST /users — client sends; server generates id + timestamps
type CreateUserRequest = Omit<User, 'id' | 'createdAt' | 'updatedAt'>
// { name: string; email: string; role: 'admin' | 'user'; bio: string | null }
// PUT /users/:id — full update (minus server fields)
type UpdateUserRequest = Omit<User, 'id' | 'createdAt' | 'updatedAt'>
// same as Create here, but semantically different
// PATCH /users/:id — partial update
type PatchUserRequest = Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>
// { name?: string; email?: string; role?: ...; bio?: ... }
// GET /users — list response (summary)
type UserSummary = Pick<User, 'id' | 'name' | 'role'>
// { id: string; name: string; role: 'admin' | 'user' }
// All derived from User — add a field to User and it propagates3. Partial for PATCH Requests
// PATCH handler — accept any subset of updatable fields
async function patchUser(
userId: string,
updates: Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>
): Promise<User> {
// Only update provided fields
if (Object.keys(updates).length === 0) {
throw new Error('No fields to update')
}
return db.users.update(userId, updates)
}
// Valid calls
await patchUser('1', { name: 'Alice Updated' })
await patchUser('1', { bio: null, role: 'admin' })
// TypeScript error — id is excluded
// await patchUser('1', { id: '2' }) ← TS error4. Record for Lookup Tables and Grouped Data
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'
// Lookup table by ID — O(1) access
const usersById: Record<string, User> = {}
users.forEach(u => { usersById[u.id] = u })
// Count by status — typed enum-like keys
const countByStatus: Record<OrderStatus, number> = {
pending: 0, processing: 0, shipped: 0, delivered: 0, cancelled: 0
}
orders.forEach(o => countByStatus[o.status]++)
// Configuration flags — all boolean values
type FeatureFlags = Record<string, boolean>
// Arbitrary JSON — more precise than any
type JsonValue = string | number | boolean | null | JsonObject | JsonArray
type JsonObject = { [key: string]: JsonValue }
type JsonArray = JsonValue[]5. Extract and Exclude for Union Types
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'
// Terminal states only
type TerminalStatus = Extract<OrderStatus, 'delivered' | 'cancelled'>
// → 'delivered' | 'cancelled'
// Active states (exclude terminal)
type ActiveStatus = Exclude<OrderStatus, 'delivered' | 'cancelled'>
// → 'pending' | 'processing' | 'shipped'
// Remove null/undefined
type StringField = string | null | undefined
type NonNullStringField = NonNullable<StringField>
// → string
// Use in narrowed context
function processActiveOrder(order: User & { status: ActiveStatus }) {
// status is guaranteed not to be delivered or cancelled
}
// Filter + narrow in one step
const activeOrders = orders.filter(
(o): o is typeof o & { status: ActiveStatus } =>
o.status !== 'delivered' && o.status !== 'cancelled'
)6. DeepPartial for Nested Config
// Custom DeepPartial — makes all nested properties optional
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T
interface AppConfig {
server: { port: number; timeout: number }
database: { host: string; port: number; name: string }
logging: { level: 'debug' | 'info' | 'warn' | 'error'; format: 'json' | 'text' }
}
// Only override what you need
function configure(overrides: DeepPartial<AppConfig>): AppConfig {
return deepMerge(defaultConfig, overrides) as AppConfig
}
// Valid — only specify changed keys
configure({ server: { port: 8080 } })
configure({ logging: { level: 'debug' } })
// type-fest provides a well-tested version
import type { PartialDeep } from 'type-fest'
function configure2(overrides: PartialDeep<AppConfig>): AppConfig { ... }7. ReturnType and Awaited for Inferred Response Types
// Infer the return type from a fetch function without repeating the type
async function fetchUser(id: string) {
const res = await fetch(`/api/users/${id}`)
return res.json() as Promise<User>
}
// In another file — type the response without importing User
type FetchUserResult = Awaited<ReturnType<typeof fetchUser>>
// → User
// For an API client object
const api = {
getUser: (id: string) => fetch(`/api/users/${id}`).then(r => r.json() as Promise<User>),
listUsers: () => fetch('/api/users').then(r => r.json() as Promise<User[]>),
}
type ApiClient = typeof api
type GetUserResult = Awaited<ReturnType<ApiClient['getUser']>> // → User
type ListUsersResult = Awaited<ReturnType<ApiClient['listUsers']>> // → User[]Frequently Asked Questions
What does Partial<T> do and when should I use it for JSON?
Partial<T> makes all properties of type T optional (adds ? to every property). It is the correct TypeScript type for PATCH request bodies — where the client sends only the fields they want to update, not the complete object. Example: interface User { id: number; name: string; email: string }. A PATCH /users/:id handler accepts Partial<User> — the body may contain name only, email only, or both, but never needs to include id (the route param). Partial is also useful for test fixtures (build an object incrementally), configuration with defaults (merge Partial<Config> with default Config), and state update functions (setState(prev => ({ ...prev, ...update })) where update is Partial<State>). Important: Partial only makes the top-level properties optional — nested objects are not affected. Use DeepPartial (a custom type) when you need nested optionality.
How do I use Pick and Omit to shape JSON API request types?
Pick<T, K> creates a new type with only the specified keys from T. Omit<T, K> creates a new type with all keys from T except the specified ones. They are ideal for modeling API request types from a base entity type. For a CREATE endpoint: type CreateUserRequest = Omit<User, "id" | "createdAt" | "updatedAt"> — the client sends everything except server-generated fields. For a list response summary: type UserSummary = Pick<User, "id" | "name" | "avatarUrl"> — the API returns a lightweight object for lists, and the full object for detail views. Pick vs Omit choice: use Pick when you want to specify exactly which fields to keep (fewer fields than the original). Use Omit when you want to exclude a small number of fields (keeping most of the original). Omit is more resilient to interface additions — new fields are automatically included, whereas Pick requires manual updates.
What is Record<K, V> and how do I use it for JSON objects?
Record<K, V> is equivalent to { [key in K]: V } — an object type where all keys are of type K and all values are of type V. It is the preferred way to type JSON objects with known key sets. For a fixed set of keys: type StatusCounts = Record<"pending" | "shipped" | "delivered" | "cancelled", number>. For dynamic keys: type UserById = Record<string, User>. For numeric keys: Record<number, string>. Record is cleaner than { [key: string]: V } because it explicitly documents what the keys represent. Common use cases: lookup tables by ID (Record<string, User>), grouped data (Record<Status, Order[]>), translation files (Record<string, string>), feature flags (Record<string, boolean>). Record<string, unknown> is the correct type for an arbitrary JSON object when you do not know its shape — prefer this over object or {} (which allows non-objects in TypeScript).
How do I create a DeepPartial type for nested JSON objects?
TypeScript does not have a built-in DeepPartial, but it is easy to define: type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T. This recursively makes all nested properties optional. Use it for configuration merging: function mergeConfig<T>(defaults: T, overrides: DeepPartial<T>): T — the override can specify only the nested fields you want to change. For an alternative without a custom type, use deep merge libraries that accept Partial at the call site. DeepPartial is also in the type-fest package: import type { PartialDeep } from "type-fest". PartialDeep from type-fest handles edge cases like readonly arrays, Maps, Sets, and circular references that a simple recursive definition misses. For API update requests with deeply nested objects, DeepPartial is the most ergonomic type — but be aware that it makes all fields optional including ones that should be required if present, so add refinement with conditional types for stricter needs.
How does Readonly<T> prevent accidental JSON mutation?
Readonly<T> makes all properties of T read-only — TypeScript will produce a compile error if you try to assign to them. For JSON responses that should not be mutated: const user: Readonly<User> = await fetchUser(id). Attempting user.name = "Bob" produces a TypeScript error. For arrays: ReadonlyArray<T> or readonly T[] prevents push(), pop(), and index assignment. For deeply nested immutability, use the Readonly mapped type recursively: type DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P] }. In React, prop types are conventionally Readonly to prevent child components from mutating props. Object.freeze() is the runtime equivalent — but it only makes the object shallowly immutable, and TypeScript does not automatically narrow a frozen object to Readonly. For production code, using immutable data patterns with structuredClone for copies is more reliable than relying solely on TypeScript readonly.
How do I type a JSON object where values can be multiple types?
For a JSON object with heterogeneous values, use Record with a union value type or a union type with discriminated keys. For arbitrary mixed values: Record<string, string | number | boolean | null>. For a typed configuration object where known keys have specific types: use an interface with specific property types for known keys and an index signature for additional keys (note: TypeScript requires the index signature type to be compatible with all specific property types). For a JSON object that represents one of several known shapes, use a discriminated union: type Config = | { type: "database"; host: string; port: number } | { type: "redis"; url: string }. For JSON that could be any valid JSON value (including nested objects and arrays), use the JsonValue type from type-fest: type JsonValue = string | number | boolean | null | JsonObject | JsonArray. This is more precise than any for functions that accept arbitrary JSON.
What is Extract and Exclude and how do I use them with JSON types?
Extract<T, U> selects the union members from T that are assignable to U. Exclude<T, U> removes the union members from T that are assignable to U. For JSON status fields: type OrderStatus = "pending" | "processing" | "shipped" | "delivered" | "cancelled". type ActiveStatus = Extract<OrderStatus, "pending" | "processing" | "shipped"> — produces "pending" | "processing" | "shipped". type TerminalStatus = Exclude<OrderStatus, "pending" | "processing"> — produces "shipped" | "delivered" | "cancelled". These compose with other utility types: type ActiveOrder = Omit<Order, "status"> & { status: ActiveStatus }. NonNullable<T> is a shorthand for Exclude<T, null | undefined> — use it when you know a nullable API field is present: function process(user: User) { const name: NonNullable<typeof user.bio> = user.bio! }.
How do I model CRUD API types using TypeScript utility types?
A complete CRUD type system built from a single base interface: interface User { id: string; name: string; email: string; role: "admin" | "user"; createdAt: string; updatedAt: string; deletedAt: string | null }. Create request (client sends, server generates id + timestamps): type CreateUserRequest = Omit<User, "id" | "createdAt" | "updatedAt" | "deletedAt">. Update (PUT) request (full object minus server fields): type UpdateUserRequest = Omit<User, "id" | "createdAt" | "updatedAt">. Patch (PATCH) request (partial update): type PatchUserRequest = Partial<Omit<User, "id" | "createdAt" | "updatedAt">>. List response summary: type UserSummary = Pick<User, "id" | "name" | "role">. This pattern keeps all types derived from one source of truth — when you add a field to User, all derived types update automatically. Use type assertions to ensure the types are used correctly: type _AssertCreateHasEmail = CreateUserRequest extends { email: string } ? true : never.
Further reading and primary sources
- TypeScript Utility Types Reference — Official TypeScript handbook page covering all built-in utility types
- type-fest on npm — Essential TypeScript types: PartialDeep, JsonValue, SetRequired, and 100+ more
- JSON to TypeScript Interface (Jsonic) — How to generate TypeScript interfaces from JSON with quicktype and manually
- Parse JSON in TypeScript (Jsonic) — Runtime-safe JSON parsing with Zod, unknown type, and type guards
- Zod Schema Validation (Jsonic) — Zod infers TypeScript types that compose well with utility types