TypeScript Utility Types Mastery: Partial, Pick, Omit, and Record
Why Am I Rewriting the Same Type Again?
Every time I built a user-related feature, I found myself writing types like this.
interface User {
id: string;
email: string;
name: string;
age: number;
createdAt: Date;
}
interface UserCreate {
email: string;
name: string;
age: number;
}
interface UserUpdate {
email?: string;
name?: string;
age?: number;
}
interface UserResponse {
id: string;
email: string;
name: string;
}
Something felt off. I kept repeating the same fields, and whenever I added a new field to User, I had to manually update all the other types. Adding phoneNumber meant touching four different places.
I was copy-pasting types. Duplication in code is a red flag, and types are no different, right? That's when I discovered utility types.
Assembling Types Like LEGO Blocks
Utility types are tools that transform existing types into new ones. Like assembling LEGO blocks, you can pick specific parts, make fields optional, or combine types from what's already there.
Once I learned TypeScript's built-in utility types, my type definitions became much cleaner. Most importantly, changing one type automatically updated all related types, making maintenance so much easier.
Partial<T>: Making Everything Optional
I use this most often for update forms. Users might change just their email, or just their name, so all fields need to be optional.
interface User {
id: string;
email: string;
name: string;
age: number;
}
// Before: manually adding ? everywhere
interface UserUpdate {
id?: string;
email?: string;
name?: string;
age?: number;
}
// After: Partial does it in one shot
type UserUpdate = Partial<User>;
function updateUser(id: string, updates: Partial<User>) {
// { email: "new@example.com" } works
// { name: "New Name", age: 30 } also works
}
Think of it as automatically adding ? to every property. It's perfect for config objects or PATCH requests.
Pick<T, K>: Selecting Only What You Need
Use this when you only need a subset of a type. It's useful for API responses where you want to exclude sensitive info, or when creating summary data with specific fields.
interface User {
id: string;
email: string;
name: string;
password: string;
createdAt: Date;
}
// Public profile: just name and ID
type PublicProfile = Pick<User, 'id' | 'name'>;
// Login form: email and password only
type LoginForm = Pick<User, 'email' | 'password'>;
const profile: PublicProfile = {
id: '123',
name: 'John',
// email: 'test@test.com' // Error! PublicProfile doesn't have email
};
Like picking specific fruits from a basket, you select only the fields you need from a type.
Omit<T, K>: Removing What You Don't Want
This is the opposite of Pick. Use it when you want "everything except this" behavior. Common for create requests where the server auto-generates certain fields.
interface User {
id: string;
email: string;
name: string;
createdAt: Date;
updatedAt: Date;
}
// Create request: server generates id and dates
type UserCreate = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
const newUser: UserCreate = {
email: 'new@example.com',
name: 'New User',
// Adding id or createdAt causes an error
};
Choosing between Pick and Omit is simple. If you need fewer fields, use Pick. If you need to exclude fewer fields, use Omit.
Record<K, V>: Dynamic Key-Value Objects
Use this to specify types for object keys and values. Perfect for config maps, lookup tables, and cache objects where keys aren't known upfront.
// User ID as key, User object as value
type UserMap = Record<string, User>;
const users: UserMap = {
'user-1': { id: 'user-1', email: 'a@test.com', name: 'Alice', age: 25, createdAt: new Date() },
'user-2': { id: 'user-2', email: 'b@test.com', name: 'Bob', age: 30, createdAt: new Date() },
};
// Restricting to specific keys
type Theme = 'light' | 'dark' | 'auto';
type ThemeConfig = Record<Theme, { background: string; text: string }>;
const themeConfig: ThemeConfig = {
light: { background: '#fff', text: '#000' },
dark: { background: '#000', text: '#fff' },
auto: { background: '#f5f5f5', text: '#333' },
// 'custom' or other keys cause an error
};
It's more explicit and readable than index signatures like { [key: string]: Value }.
Required<T>: Making Everything Mandatory
The opposite of Partial. It makes all optional fields required.
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
// After validating all values are set at runtime
type ValidatedConfig = Required<Config>;
function startServer(config: ValidatedConfig) {
// config.host is guaranteed to exist (no ?)
console.log(config.host.toUpperCase()); // Safe
}
Useful when you accept optional config, fill in defaults, then want a fully populated config object.
Readonly<T>: Creating Immutable Objects
Prevents modification after creation. Use it for constant configs or event objects that shouldn't change.
interface Point {
x: number;
y: number;
}
const origin: Readonly<Point> = { x: 0, y: 0 };
origin.x = 10; // Error! Can't modify readonly property
// Works with arrays too
const numbers: ReadonlyArray<number> = [1, 2, 3];
numbers.push(4); // Error!
Catches accidental mutations at compile time.
ReturnType<T> and Parameters<T>: Extracting Types from Functions
These extract types from function signatures. Useful when you define a function first, then want to reuse its return type or parameter types.
function createUser(email: string, name: string) {
return {
id: Math.random().toString(),
email,
name,
createdAt: new Date(),
};
}
// Extract function return type
type User = ReturnType<typeof createUser>;
// { id: string; email: string; name: string; createdAt: Date }
// Extract function parameter types
type CreateUserParams = Parameters<typeof createUser>;
// [email: string, name: string]
// Just the first parameter
type Email = Parameters<typeof createUser>[0]; // string
When a function returns a complex object and you want to use that as a type, you can avoid duplication this way.
Real-World Pattern: Deriving All Types from One Base
Let's revisit the original problem. Rewriting User-related types with utility types looks like this.
// 1. Define just one base type
interface User {
id: string;
email: string;
name: string;
age: number;
password: string;
createdAt: Date;
updatedAt: Date;
}
// 2. Derive everything else with utility types
type UserCreate = Omit<User, 'id' | 'createdAt' | 'updatedAt'>;
type UserUpdate = Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>;
type UserResponse = Omit<User, 'password'>;
type PublicProfile = Pick<User, 'id' | 'name'>;
type UsersMap = Record<string, User>;
// 3. Modifying User automatically updates other types
// Adding phoneNumber automatically reflects in UserCreate, UserUpdate!
Type definitions became declarative. The intent is clear: "UserCreate is User without id and date fields." And when you add a new field to User, all related types update automatically.
Combining Utility Types
Combining multiple utility types lets you create complex types concisely.
// Some fields required, some optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface Product {
id: string;
name: string;
price: number;
description: string;
}
// Make only price and description optional
type ProductDraft = PartialBy<Product, 'price' | 'description'>;
const draft: ProductDraft = {
id: '1',
name: 'Laptop',
// price, description are optional
};
You can even create custom utility types like this.
How They Work: Mapped Types
Utility types are internally implemented using mapped types. They iterate over type properties and transform them.
// Partial's actual implementation
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Pick's actual implementation
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Record's actual implementation
type Record<K extends keyof any, T> = {
[P in K]: T;
};
[P in keyof T] means iterating over all keys of T. Like an array's map function, it processes each field of a type. Adding ? makes it optional, selecting specific keys works like Pick.
Understanding this principle lets you create your own utility types.
// Wrapping all values in Promise
type Promisify<T> = {
[P in keyof T]: Promise<T[P]>;
};
interface SyncAPI {
getUser: User;
getPost: Post;
}
type AsyncAPI = Promisify<SyncAPI>;
// { getUser: Promise<User>; getPost: Promise<Post> }
Summary: Types Are Reusable Too
Before using utility types, I was copy-pasting type definitions. Changing one meant updating multiple places, and mistakes were common.
After learning utility types, I realized types can be reused just like code. Define one solid base type, then compose the rest with Partial, Pick, Omit, and Record.
Key Takeaways:
Partial<T>: Make all fields optional (updates, patches)Required<T>: Make all fields required (validated after defaults)Pick<T, K>: Select specific fields (public data, summaries)Omit<T, K>: Exclude specific fields (create requests, remove sensitive data)Record<K, V>: Key-value maps (caches, configs, lookup tables)Readonly<T>: Immutable objects (constants, events)ReturnType<T>,Parameters<T>: Extract types from function signatures
Cleaner type definitions made code more readable, and changing one place made maintenance easier. This is why we use TypeScript in the first place. The type system makes refactoring safe, and utility types make it even easier.
Now whenever I'm about to write a similar type, I first ask myself: "Can I make this with a utility type instead?"