DEV Community

Carlos Oliva Pascual
Carlos Oliva Pascual

Posted on • Originally published at stacknotice.com

TypeScript Utility Types: The Complete Guide (2026)

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'>>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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,
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 },
  })
}
Enter fullscreen mode Exit fullscreen mode

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'>
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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>>
Enter fullscreen mode Exit fullscreen mode

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>
  }
}
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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'>>
Enter fullscreen mode Exit fullscreen mode

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'>>
Enter fullscreen mode Exit fullscreen mode

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)