Every TypeScript developer uses Pick, Omit, Partial, and Record. But ask them how Omit is actually defined, and you'll get blank stares half the time.
That's not a criticism — it means the abstractions are working. But when you understand how these types are built, three things happen:
- You stop guessing which utility to use
- You can combine them to solve real problems
- You can write your own when the built-ins aren't enough
Let's walk through every built-in utility type from the inside out.
The Engine: Mapped Types and Conditional Types
Before we touch a single utility, you need two mental models.
Mapped types transform an object type by iterating over its keys:
// Takes an object type T, returns a new type with same keys but values as strings
type Stringify<T> = {
[K in keyof T]: string
}
// Usage:
type User = { id: number; name: string; email: string }
type StringUser = Stringify<User>
// { id: string; name: string; email: string }
Conditional types select between two types based on a condition:
type IsString<T> = T extends string ? true : false
type A = IsString<"hello"> // true
type B = IsString<42> // false
Every utility type in this article is built from these two primitives (plus keyof, typeof, and indexing). No magic.
1. Partial<T> — Make Every Property Optional
What it does: Takes a type and returns a new type where every property is optional.
How it works:
// Real definition in lib.es5.d.ts:
type Partial<T> = {
[P in keyof T]?: T[P]
}
The ? modifier is what makes each property optional. That's it — a single mapped type with an optional marker.
Real-world use:
interface UserConfig {
theme: "light" | "dark"
fontSize: number
notifications: boolean
}
function applyConfig(updates: Partial<UserConfig>) {
// Merge incoming partial updates with current config
return { ...currentConfig, ...updates }
}
// Usage:
applyConfig({ theme: "dark" })
applyConfig({ fontSize: 14, notifications: false })
applyConfig({}) // Also valid — no changes
This is the most common argument type for PATCH endpoints and setState-style updates.
2. Required<T> — Make Every Property Mandatory
What it does: The inverse of Partial. Every optional property becomes required.
How it works:
type Required<T> = {
[P in keyof T]-?: T[P]
}
The -? syntax removes the optional modifier. The - prefix strips modifiers instead of adding them.
Real-world use:
interface DraftPost {
title?: string
body?: string
tags?: string[]
}
function publishPost(post: Required<DraftPost>) {
// All fields must be filled in before publishing
// publishPost({ title: "Hi" }) // ❌ Error — body and tags required
}
3. Readonly<T> — Lock Down Properties
What it does: Marks every property as readonly.
How it works:
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
Real-world use:
function freezeConfig<T extends object>(config: T): Readonly<T> {
return Object.freeze(config)
}
const APP_CONFIG = freezeConfig({
apiUrl: "https://api.example.com",
timeout: 5000,
})
// APP_CONFIG.apiUrl = "https://evil.com" // ❌ Cannot assign to readonly
Combine with Partial for the classic "configuration that can be partially set once" pattern:
type ImmutableConfig<T> = Readonly<Partial<T>>
4. Pick<T, K> — Select Specific Keys
What it does: Creates a type with only the keys you specify from T.
How it works:
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
The constraint K extends keyof T ensures you can only pick keys that actually exist on T. TypeScript catches typos at compile time.
Real-world use:
interface User {
id: string
name: string
email: string
passwordHash: string
ssn: string
role: "admin" | "user"
}
// Public profile — never expose sensitive fields
type PublicUser = Pick<User, "id" | "name" | "email" | "role">
function getPublicProfile(user: User): PublicUser {
return {
id: user.id,
name: user.name,
email: user.email,
role: user.role,
// passwordHash and ssn are simply not returned
}
}
5. Record<K, T> — Build Object Types from Scratch
What it does: Creates an object type where keys are type K and values are type T.
How it works:
type Record<K extends keyof any, T> = {
[P in K]: T
}
keyof any is string | number | symbol — the set of valid JavaScript object keys.
Real-world use — mapping enums to data:
type HttpStatus = 200 | 201 | 400 | 401 | 500
type StatusMessages = Record<HttpStatus, string>
const messages: StatusMessages = {
200: "OK",
201: "Created",
400: "Bad Request",
401: "Unauthorized",
500: "Internal Server Error",
}
TypeScript will enforce that you include every key in the union:
const badMessages: StatusMessages = {
200: "OK",
// ❌ Error: 201, 400, 401, 500 are missing
}
Key pattern — dictionaries:
type UserMap = Record<string, User>
const users: UserMap = {}
users["abc123"] = { id: "abc123", name: "Alice", email: "alice@example.com" }
But Record<string, T> is a dictionary — it accepts any string key. For stricter mappings, use a union type as K.
6. Exclude<T, U> — Remove from a Union
What it works on: Union types (not object types).
How it works:
type Exclude<T, U> = T extends U ? never : T
This distributes over the union T. For each member of T:
- If it extends
U, it becomesnever(removed) - Otherwise, it stays
type Shape = "circle" | "square" | "triangle" | "rectangle"
// Remove 'triangle' from the union
type Polygon = Exclude<Shape, "triangle">
// "circle" | "square" | "rectangle"
// Remove multiple
type NoCircles = Exclude<Shape, "circle" | "square">
// "triangle" | "rectangle"
The distributive property is key here. Exclude<string | number | boolean, string | number> evaluates as:
(string extends string | number ? never : string) → never
| (number extends string | number ? never : number) → never
| (boolean extends string | number ? never : boolean) → boolean
// Result: boolean
7. Extract<T, U> — Keep Only Matching Union Members
What it does: The inverse of Exclude — keeps only the members of T that are assignable to U.
How it works:
type Extract<T, U> = T extends U ? T : never
Real-world use — event type narrowing:
type AllEvents =
| { type: "click"; x: number; y: number }
| { type: "keypress"; key: string }
| { type: "focus" }
| { type: "blur" }
// Get only the event types that have a specific shape
type ClickEvents = Extract<AllEvents, { type: "click" }>
// { type: "click"; x: number; y: number }
type InputEvents = Extract<AllEvents, { type: "keypress" | "focus" }>
// { type: "keypress"; key: string } | { type: "focus" }
8. Omit<T, K> — Remove Specific Keys
What it does: The inverse of Pick — returns a type with specific keys removed.
How it works:
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
This is the most elegant composition of utility types. It:
- Gets all keys of
Twithkeyof T - Removes
Kfrom that union withExclude - Picks the remaining keys with
Pick
Real-world use — remove internal fields before serialization:
interface InternalTodo {
id: string
title: string
completed: boolean
_version: number
_syncStatus: "pending" | "synced"
_createdBy: string
}
// Public API — strip internal fields
type APITodo = Omit<InternalTodo, "_version" | "_syncStatus" | "_createdBy">
// { id: string; title: string; completed: boolean }
Note: In TypeScript 5.x+,
Omitwas updated soKno longer needs to extendkeyof T— you can pass keys that don't exist onTwithout error. This makes it safer for generic code where you're not sure which keys exist.
Bonus: The Conditional Utility Types
These use conditional types with infer to extract information from function and constructor types.
Parameters<T> — Get Function Parameter Types
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never
function createUser(name: string, age: number, email: string) {
return { name, age, email }
}
type CreateUserArgs = Parameters<typeof createUser>
// [name: string, age: number, email: string]
ReturnType<T> — Get Function Return Type
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
type CreateUserReturn = ReturnType<typeof createUser>
// { name: string; age: number; email: string }
ConstructorParameters<T> and InstanceType<T>
These work on constructor signatures:
class Database {
constructor(public host: string, public port: number) {}
}
type DBParams = ConstructorParameters<typeof Database>
// [host: string, port: number]
type DBInstance = InstanceType<typeof Database>
// Database
NonNullable<T> — Remove Null and Undefined
type NonNullable<T> = T extends null | undefined ? never : T
type Maybe = string | null | undefined
type Definitely = NonNullable<Maybe>
// string
Real-World Patterns
Pattern 1: Selective Partial (Deep Partial Update)
The built-in Partial is shallow. For nested updates, you need a recursive version:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
interface NestedConfig {
server: { host: string; port: number }
database: { url: string; poolSize: number }
}
function patchConfig(update: DeepPartial<NestedConfig>) {
// patchConfig({ server: { host: "new-host" } }) // ✅ Works deeply
}
Pattern 2: Pick by Value Type
Sometimes you want to pick keys based on what value type they hold, not by name:
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K]
}
interface Entity {
id: string
name: string
createdAt: Date
updatedAt: Date
metadata: Record<string, unknown>
}
type DateFields = PickByValue<Entity, Date>
// { createdAt: Date; updatedAt: Date }
This uses key remapping via as (TypeScript 4.1+), which lets you filter keys inside the mapped type.
Pattern 3: Make Specific Keys Required
Required<T> makes everything required. What if you only need a subset?
type WithRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>
interface Draft {
title?: string
body?: string
tags?: string[]
}
type DraftWithTitle = WithRequired<Draft, "title">
// title is required; body and tags stay optional
Pattern 4: Substitute Property Types
Need to change the type of one property without touching the rest?
type Override<T, R extends Partial<Record<keyof T, unknown>>> = {
[P in keyof T]: P extends keyof R ? R[P] : T[P]
}
interface Feature {
name: string
enabled: boolean
version: number
}
// Change `version` from number to string
type FeatureUI = Override<Feature, { version: string }>
// { name: string; enabled: boolean; version: string }
Common Gotchas
🚨 Omit with unions behaves unexpectedly
When T is a union, Omit distributes over it:
type Result =
| { status: "success"; data: string }
| { status: "error"; message: string }
type WithoutStatus = Omit<Result, "status">
// { data: string } | { message: string }
// Both branches survive — only the status key is removed from each
🚨 Readonly is shallow
interface User {
name: string
address: { city: string; zip: string }
}
const user: Readonly<User> = { name: "Alice", address: { city: "NYC", zip: "10001" } }
// user.name = "Bob" // ❌ Error
// user.address.city = "LA" // ✅ No error — address is still mutable!
For truly immutable types, you need a recursive DeepReadonly.
🚨 Partial from the right place
If you have a union of objects, Partial distributes:
type State = { type: "loading" } | { type: "loaded"; data: string }
type PartialState = Partial<State>
// { type?: "loading" } | { type?: "loaded"; data?: string }
This is rarely what you want. Map over the union explicitly instead.
Summary
| Utility | What It Does | Built From |
|---|---|---|
Partial<T> |
Makes all props optional | Mapped type with ?
|
Required<T> |
Makes all props required | Mapped type with -?
|
Readonly<T> |
Makes all props readonly | Mapped type with readonly
|
Pick<T, K> |
Selects specific keys | Mapped type constrained to K
|
Record<K, T> |
Creates key-value type | Mapped type over K
|
Exclude<T, U> |
Removes from union | Conditional (distributes) |
Extract<T, U> |
Keeps matching union members | Conditional (distributes) |
Omit<T, K> |
Removes specific keys |
Pick + Exclude
|
Parameters<T> |
Gets function params |
infer in conditional |
ReturnType<T> |
Gets function return |
infer in conditional |
NonNullable<T> |
Removes null/undefined
|
Conditional |
The built-in utility types aren't magic — they're just mapped types and conditional types with clear names. Once you understand that, you can read their definitions in your editor (Cmd+Click in VS Code) and even write your own.
And when the built-ins aren't enough, you now have the primitives to build exactly what you need.
Published by Kai Thorne. I write about TypeScript, Python, and developer workflows.

























