
The TypeScript Utility Types I Actually Use Every Week
I've been writing TypeScript for about five years and there's a specific feeling I get when I see someone writing types that could be derived like they've manually rewritten a type that TypeScript could have computed for them.
The utility types exist to let you derive types from other types. When you learn them, you write less type maintenance code and your types stay in sync automatically.
Here are the ones that earn their keep.
Partial<T> and Required<T>
You probably know these. Partial makes every property optional, Required makes every property required.
Where they're actually useful: update operations.
typescriptinterface User { id: string name: string email: string bio: string avatarUrl: string } // For PATCH endpoints only send what you're changing type UserUpdate = Partial<Omit<User, 'id'>> async function updateUser(id: string, updates: UserUpdate): Promise<User> { // updates can have any subset of fields except id return fetch(`/api/users/${id}`, { method: 'PATCH', body: JSON.stringify(updates), }).then(r => r.json()) } updateUser('123', { name: 'New Name' }) // fine updateUser('123', { email: 'new@email.com' }) // fine updateUser('123', { name: 'x', bio: 'hello' }) // fine updateUser('123', { id: '456' }) // type error — good
The Omit<User, 'id'> before Partial is important. You don't want the ID to be updatable even if you accidentally pass it.
Pick<T, K> and Omit<T, K>
Pick creates a type with only the specified properties. Omit creates a type with everything except the specified properties.
The pattern I use most is creating "view" types from a larger model:
typescriptinterface Post { id: string title: string body: string authorId: string author: User tags: Tag[] publishedAt: Date | null editedAt: Date | null viewCount: number metadata: Record<string, unknown> } // For the list view, we don't need body or metadata type PostSummary = Pick<Post, 'id' | 'title' | 'author' | 'tags' | 'publishedAt'> // For the API response, strip internal fields type PostPublic = Omit<Post, 'authorId' | 'metadata'>
This pattern keeps you from accidentally exposing internal fields and from sending more data than needed over the wire.
ReturnType<T> and Parameters<T>
These derive types from functions. I use them when I don't control the source type — like when a library returns something and I want to type a variable to hold it.
typescriptimport { createClient } from '@supabase/supabase-js' // I want to store the client but I don't want to import its type separately const supabase = createClient(url, key) type SupabaseClient = typeof supabase // Or: get the type of data a function returns async function fetchUser(id: string) { return db.user.findUnique({ where: { id }, include: { posts: true } }) } type UserWithPosts = Awaited<ReturnType<typeof fetchUser>> // Now this type stays in sync with the function automatically
The Awaited<> wrapper is needed when the function is async. ReturnType gives you Promise<User | null>, Awaited unwraps it to User | null.
Record<K, V>
Record creates an object type with specific key and value types. It's cleaner than { [key: string]: V } and lets you constrain the keys to a union:
typescripttype Status = 'pending' | 'processing' | 'complete' | 'failed' // An object where every status must have a label and color const STATUS_CONFIG: Record<Status, { label: string; color: string }> = { pending: { label: 'Pending', color: 'gray' }, processing: { label: 'Processing', color: 'blue' }, complete: { label: 'Complete', color: 'green' }, failed: { label: 'Failed', color: 'red' }, } // TypeScript will error if you add a new Status but forget to add it here
This is the "exhaustive record" pattern and it saves you from runtime errors when you add new enum values and forget to update the config objects.
Extract<T, U> and Exclude<T, U>
These filter union types. Extract keeps only the members assignable to U. Exclude removes them.
typescripttype HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' // Only the methods that modify data type MutationMethod = Extract<HTTPMethod, 'POST' | 'PUT' | 'PATCH' | 'DELETE'> // → 'POST' | 'PUT' | 'PATCH' | 'DELETE' // Only the read methods type ReadMethod = Exclude<HTTPMethod, 'POST' | 'PUT' | 'PATCH' | 'DELETE'> // → 'GET' | 'HEAD' | 'OPTIONS'
A more practical example — filtering a discriminated union:
typescripttype Event = | { type: 'click'; x: number; y: number } | { type: 'keypress'; key: string } | { type: 'resize'; width: number; height: number } | { type: 'scroll'; scrollY: number } // Only pointer events type PointerEvent = Extract<Event, { type: 'click' | 'scroll' }> // → { type: 'click'; x: number; y: number } | { type: 'scroll'; scrollY: number }
NonNullable<T>
Removes null and undefined from a type. Useful when you've already done the null check but TypeScript doesn't know:
typescriptfunction processUser(user: User | null | undefined) { if (!user) throw new Error('User required') // user is still User | null | undefined here in older TS // but you know it's defined — narrow it explicitly const definiteUser: NonNullable<typeof user> = user } // More commonly: derive non-nullable versions of types type Config = { apiKey: string | null baseUrl: string | undefined timeout: number } type ResolvedConfig = { [K in keyof Config]: NonNullable<Config[K]> } // → { apiKey: string; baseUrl: string; timeout: number }
Combining Them: A Form State Pattern
Here's a pattern I use constantly deriving form state from a data model:
typescriptinterface Product { id: string name: string price: number description: string categoryId: string publishedAt: Date | null internalNotes: string sku: string } // What the create form captures (no id, no internal fields) type CreateProductForm = Omit<Product, 'id' | 'publishedAt' | 'internalNotes'> // What the edit form captures (everything optional except id) type EditProductForm = Required<Pick<Product, 'id'>> & Partial<Omit<Product, 'id' | 'internalNotes'>> // Validation errors same shape as the form, but string messages type FormErrors<T> = Partial<Record<keyof T, string>> type CreateProductErrors = FormErrors<CreateProductForm> // → { name?: string; price?: string; description?: string; ... }
When Product changes, CreateProductForm, EditProductForm, and the error types all update automatically. No manual sync.
The One That Takes a Minute to Understand: infer
infer isn't a utility type but it's how many utility types are built. It lets you extract a type from within a conditional type:
typescript// This is what ReturnType looks like internally type MyReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never // Extract the element type from an array type ElementType<T> = T extends (infer E)[] ? E : never type PostId = ElementType<string[]> // → string type TagId = ElementType<Tag[]> // → Tag
You won't write infer every day, but when you need to derive a type from something nested function parameters, promise resolution, array elements it's the right tool.
The moment it clicks is when you realize that TypeScript types are a programming language of their own, and infer is basically pattern matching on types. After that, you start seeing what's possible.