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 }
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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]
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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]
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode
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]
}
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
Combine with Partial for the classic “configuration that can be partially set once” pattern:
type ImmutableConfig<T> = Readonly<Partial<T>>
Enter fullscreen mode Exit fullscreen mode
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]
}
Enter fullscreen mode Exit fullscreen mode
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
}
}
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode
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",
}
Enter fullscreen mode Exit fullscreen mode
TypeScript will enforce that you include every key in the union:
const badMessages: StatusMessages = {
200: "OK",
// ❌ Error: 201, 400, 401, 500 are missing
}
Enter fullscreen mode Exit fullscreen mode
Key pattern — dictionaries:
type UserMap = Record<string, User>
const users: UserMap = {}
users["abc123"] = { id: "abc123", name: "Alice", email: "[email protected]" }
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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"
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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" }
Enter fullscreen mode Exit fullscreen mode
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>>
Enter fullscreen mode Exit fullscreen mode
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 }
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
function createUser(name: string, age: number, email: string) {
return { name, age, email }
}
type CreateUserArgs = Parameters<typeof createUser>
// [name: string, age: number, email: string]
Enter fullscreen mode Exit fullscreen mode
ReturnType<T> — Get Function Return Type
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
Enter fullscreen mode Exit fullscreen mode
type CreateUserReturn = ReturnType<typeof createUser>
// { name: string; age: number; email: string }
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
NonNullable<T> — Remove Null and Undefined
type NonNullable<T> = T extends null | undefined ? never : T
Enter fullscreen mode Exit fullscreen mode
type Maybe = string | null | undefined
type Definitely = NonNullable<Maybe>
// string
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode
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 }
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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 }
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
🚨 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!
Enter fullscreen mode Exit fullscreen mode
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 }
Enter fullscreen mode Exit fullscreen mode
This is rarely what you want. Map over the union explicitly instead.
Summary
Utility What It Does Built FromPartial<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.
Level up your development workflow
If this article helped you think more clearly about TypeScript, here are some resources I’ve put together:
- Prompt Creator Bundle — Developer Edition — 70+ AI prompts for debugging, code review & SaaS building. ChatGPT & Claude templates.
- Developer Prompt Pack Bundle — 4 packs in 1: debugging, API building, React, and SaaS prompts. Save 40%.
- 100+ AI Coding Prompts for ChatGPT, Claude & Cursor — Debug, refactor, and ship faster with battle-tested prompts.
Published by Kai Thorne. I write about TypeScript, Python, and developer workflows.
답글 남기기