TypeScript Utility Types: Mastering Pick, Omit, Partial, Required, and Record
TypeScript ships with powerful utility types that eliminate boilerplate.
Here's a practical guide to the ones you'll use daily.
Partial — Make All Properties Optional
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
}
// For update operations where any field can be changed
type UpdateUserDto = Partial<User>
// { id?: string; name?: string; email?: string; role?: 'admin' | 'user' }
async function updateUser(id: string, data: Partial<Omit<User, 'id'>>) {
return db.user.update({ where: { id }, data })
}
updateUser('1', { name: 'New Name' }) // only name, valid
updateUser('1', { role: 'admin' }) // only role, valid
Required — Make All Properties Required
interface Config {
apiUrl?: string
timeout?: number
retries?: number
}
// After validation, all fields are present
type ValidatedConfig = Required<Config>
// { apiUrl: string; timeout: number; retries: number }
function validateConfig(config: Config): ValidatedConfig {
return {
apiUrl: config.apiUrl ?? 'https://api.example.com',
timeout: config.timeout ?? 5000,
retries: config.retries ?? 3,
}
}
Pick — Select Specific Properties
interface User {
id: string
name: string
email: string
passwordHash: string
stripeCustomerId: string
}
// Safe to expose publicly
type PublicUser = Pick<User, 'id' | 'name' | 'email'>
// { id: string; name: string; email: string }
// Never expose passwordHash or stripeCustomerId
function toPublicUser(user: User): PublicUser {
return { id: user.id, name: user.name, email: user.email }
}
Omit — Exclude Specific Properties
// For create operations — DB generates the id
type CreateUserDto = Omit<User, 'id' | 'passwordHash'>
// { name: string; email: string; stripeCustomerId: string }
// Combine with Partial for flexible updates
type UpdateUserDto = Partial<Omit<User, 'id'>>
Record — Object with Specific Key/Value Types
// Map of user roles to permissions
type Permission = 'read' | 'write' | 'delete' | 'admin'
type Role = 'viewer' | 'editor' | 'admin'
const rolePermissions: Record<Role, Permission[]> = {
viewer: ['read'],
editor: ['read', 'write'],
admin: ['read', 'write', 'delete', 'admin'],
}
// Status map
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled'
const statusLabels: Record<OrderStatus, string> = {
pending: 'Waiting for payment',
processing: 'Preparing your order',
shipped: 'On the way',
delivered: 'Delivered',
cancelled: 'Cancelled',
}
Readonly — Immutable Type
const config: Readonly<Config> = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
}
config.timeout = 1000 // Error: Cannot assign to 'timeout' (readonly)
ReturnType and Parameters
async function getUser(id: string) {
return db.user.findUniqueOrThrow({ where: { id } })
}
// Infer the return type without duplicating it
type User = Awaited<ReturnType<typeof getUser>>
// Infer parameter types
type GetUserParams = Parameters<typeof getUser>
// [id: string]
// Useful for wrapping functions
function withLogging<T extends (...args: any[]) => any>(
fn: T
): (...args: Parameters<T>) => ReturnType<T> {
return (...args) => {
console.log('Calling', fn.name, 'with', args)
return fn(...args)
}
}
NonNullable
type MaybeUser = User | null | undefined
type DefinitelyUser = NonNullable<MaybeUser> // User
// After null checks
function processUser(user: MaybeUser) {
if (!user) throw new Error('User required')
const definite: NonNullable<typeof user> = user // User
}
Extract and Exclude
type Status = 'active' | 'inactive' | 'pending' | 'banned'
type ActiveStatuses = Extract<Status, 'active' | 'pending'>
// 'active' | 'pending'
type AllowedStatuses = Exclude<Status, 'banned'>
// 'active' | 'inactive' | 'pending'
The Ship Fast Skill Pack includes a /api skill that generates fully-typed DTOs using these patterns. $49 one-time.
Top comments (0)