TypeScript ships with a set of generic utility types that transform existing types into new ones. They eliminate repetitive type definitions and make your types more precise without writing more code.
Most developers know Partial and Pick. Most are missing Awaited, Extract, and the composed patterns that make utility types genuinely powerful.
Why Utility Types Exist
Without utility types, you copy and modify types by hand:
// Manual — goes out of sync when User changes:
interface UserUpdateInput {
email?: string
name?: string
role?: 'admin' | 'member' | 'viewer'
}
// Derived — always in sync:
type UserUpdateInput = Partial<Omit<User, 'id' | 'createdAt'>>
Partial<T>
Makes all properties optional.
async function updateUser(id: string, data: Partial<Omit<User, 'id'>>) {
return db.user.update({ where: { id }, data })
}
await updateUser('123', { name: 'Alice' }) // valid
await updateUser('123', { email: 'alice@example.com', role: 'admin' }) // valid
Required<T>
Makes all properties required — removes optional modifiers.
interface Config {
apiUrl?: string
timeout?: number
retries?: number
}
function resolveConfig(input: Config): Required<Config> {
return {
apiUrl: input.apiUrl ?? 'https://api.example.com',
timeout: input.timeout ?? 5000,
retries: input.retries ?? 3,
}
}
Readonly<T>
Makes all properties readonly — TypeScript errors on any mutation.
function getConfig(): Readonly<AppConfig> {
return { apiUrl: process.env.API_URL!, featureFlags: { newDashboard: true } }
}
const config = getConfig()
config.apiUrl = 'something' // TS Error: Cannot assign to 'apiUrl' because it is a read-only property
Pick<T, K>
Creates a type with only the selected properties.
interface User {
id: string
email: string
name: string
passwordHash: string
createdAt: Date
}
// Safe to send to client
type PublicUser = Pick<User, 'id' | 'email' | 'name'>
export async function getCurrentUser(): Promise<PublicUser | null> {
return db.user.findUnique({
where: { id: session.userId },
select: { id: true, email: true, name: true },
})
}
Omit<T, K>
The inverse — removes the listed properties.
type CreatePostInput = Omit<Post, 'id' | 'createdAt' | 'updatedAt'>
type UpdatePostInput = Partial<Omit<Post, 'id' | 'createdAt' | 'updatedAt' | 'authorId'>>
type PostResponse = Omit<Post, 'authorId'>
Pick vs Omit: Use Pick when you want a small subset. Use Omit when you want everything except a few fields.
Record<K, T>
Creates an object type with typed keys and values.
type Role = 'admin' | 'member' | 'viewer'
const permissions: Record<Role, string[]> = {
admin: ['read', 'write', 'delete'],
member: ['read', 'write'],
viewer: ['read'],
// TS error if any role key is missing
}
Better than { [key: string]: T } because it enforces exhaustive keys when K is a union.
Exclude<T, U> and Extract<T, U>
These work on union types, not object types.
type Status = 'draft' | 'published' | 'archived' | 'deleted'
type ActiveStatus = Exclude<Status, 'archived' | 'deleted'>
// 'draft' | 'published'
type InactiveStatus = Extract<Status, 'archived' | 'deleted'>
// 'archived' | 'deleted'
Real use case — filtering discriminated union events:
type AppEvent =
| { type: 'user.created'; userId: string }
| { type: 'user.deleted'; userId: string }
| { type: 'post.published'; postId: string }
type UserEvent = Extract<AppEvent, { type: `user.${string}` }>
// { type: 'user.created'; ... } | { type: 'user.deleted'; ... }
function handleUserEvent(event: UserEvent) {
// Fully type-safe — only receives user events
}
NonNullable<T>
Removes null and undefined.
const maybeUsers: (User | null)[] = await Promise.all(ids.map(getUser))
const users: User[] = maybeUsers.filter(
(u): u is NonNullable<typeof u> => u !== null
)
ReturnType<T> and Parameters<T>
async function getUser(id: string) {
return db.user.findUnique({ where: { id } })
}
type ResolvedUser = Awaited<ReturnType<typeof getUser>>
// User | null
// Get types of third-party functions without explicit imports:
import { auth } from '@clerk/nextjs/server'
type AuthResult = Awaited<ReturnType<typeof auth>>
Parameters<T> extracts parameter types — useful for wrapping functions:
function withLogging<T extends (...args: unknown[]) => unknown>(
fn: T,
name: string
): (...args: Parameters<T>) => ReturnType<T> {
return (...args: Parameters<T>) => {
console.log(`[${name}]`, args)
return fn(...args) as ReturnType<T>
}
}
Awaited<T>
Unwraps nested promises recursively.
async function fetchDashboardData() {
const [user, posts, stats] = await Promise.all([getUser(), getPosts(), getStats()])
return { user, posts, stats }
}
type DashboardData = Awaited<ReturnType<typeof fetchDashboardData>>
// { user: User; posts: Post[]; stats: Stats }
Combining Utility Types
The real power is composition:
// All derived from one source type:
type CreatePostInput = Omit<Post, 'id' | 'createdAt' | 'updatedAt'>
type UpdatePostInput = Partial<Omit<Post, 'id' | 'createdAt' | 'updatedAt' | 'authorId'>>
type PostResponse = Omit<Post, 'authorId'>
// Form values from an API type:
type ProfileFormValues = Partial<Pick<UserProfile, 'name' | 'bio' | 'avatarUrl'>>
With Zod
const userSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
role: z.enum(['admin', 'member', 'viewer']),
})
type User = z.infer<typeof userSchema>
// Derive without duplicating the schema:
type UserPatch = Partial<Omit<User, 'id'>>
Quick Reference
| Utility Type | What it does |
|---|---|
Partial<T> |
All properties optional |
Required<T> |
All properties required |
Readonly<T> |
All properties readonly |
Pick<T, K> |
Keep listed properties |
Omit<T, K> |
Remove listed properties |
Record<K, T> |
Object with typed keys |
Exclude<T, U> |
Remove union members |
Extract<T, U> |
Keep union members |
NonNullable<T> |
Remove null/undefined |
ReturnType<T> |
Return type of function |
Parameters<T> |
Parameter tuple |
Awaited<T> |
Unwrap promises |
The signal: when you find yourself copying a type definition by hand with one field changed, there's a utility type for it.
Full guide at stacknotice.com/blog/typescript-utility-types-guide-2026
Top comments (0)