TypeScript Utility Types — Complete Reference for Backend Devs

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Why This Matters

TypeScript's built-in utility types eliminate boilerplate and enforce consistency across large backend codebases. Without them, you end up manually copying interface definitions and making small, error-prone modifications. With utility types, you derive new types from existing ones at the type level — no duplication, no drift.

Backend engineers encounter utility types constantly: Partial<T> for PATCH request bodies, Omit<T, 'password'> for safe API responses, Record<string, T> for lookup maps, and ReturnType<F> for inferring service return types without importing extra interfaces. Knowing these deeply makes you dramatically more productive.

Beyond the built-ins, understanding how utility types are constructed internally unlocks the ability to write your own — which is necessary for advanced patterns like deep-partial configs, readonly domain entities, and form state management.

Object Transformation Utilities

Partial and Required

interface UserProfile {
  id: string;
  username: string;
  email: string;
  bio: string;
  avatarUrl: string;
}
 
// PATCH endpoint body — all fields optional
type UpdateProfileDto = Partial<UserProfile>;
 
// Make all fields required (reverses optionals)
type RequiredProfile = Required<UpdateProfileDto>;
 
// Real-world: partial config with required overrides
function updateProfile(
  existing: UserProfile,
  updates: Partial<UserProfile>
): UserProfile {
  return { ...existing, ...updates };
}

Pick and Omit

interface User {
  id: string;
  name: string;
  email: string;
  passwordHash: string;
  role: 'admin' | 'user';
  createdAt: Date;
}
 
// Safe response — never expose password hash
type PublicUser = Omit<User, 'passwordHash'>;
 
// Minimal auth token payload
type TokenPayload = Pick<User, 'id' | 'email' | 'role'>;
 
// Create DTO — id and timestamps set by the server
type CreateUserDto = Omit<User, 'id' | 'createdAt' | 'passwordHash'> & {
  password: string;
};
 
// Usage example
function toPublicUser(user: User): PublicUser {
  const { passwordHash, ...publicUser } = user;
  return publicUser;
}

Readonly

interface Config {
  apiUrl: string;
  timeout: number;
  retries: number;
}
 
// Prevent accidental mutation
const config: Readonly<Config> = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
};
 
// config.timeout = 10000; // Error: Cannot assign to 'timeout' because it is a read only property
 
// Deep readonly for nested objects
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
 
type FrozenConfig = DeepReadonly<{
  server: { host: string; port: number };
  db: { url: string; pool: number };
}>;

Record Type

Record<Keys, Value> creates an object type with a fixed set of keys.

type Environment = 'development' | 'staging' | 'production';
 
type EnvironmentConfig = Record<Environment, {
  apiUrl: string;
  logLevel: 'debug' | 'info' | 'warn' | 'error';
}>;
 
const configs: EnvironmentConfig = {
  development: { apiUrl: 'http://localhost:3000', logLevel: 'debug' },
  staging: { apiUrl: 'https://staging.api.com', logLevel: 'info' },
  production: { apiUrl: 'https://api.com', logLevel: 'warn' },
};
 
// Runtime lookup map
type UserById = Record<string, User>;
 
// Permission matrix
type PermissionMap = Record<'read' | 'write' | 'delete' | 'admin', boolean>;
 
const adminPermissions: PermissionMap = {
  read: true,
  write: true,
  delete: true,
  admin: true,
};

Type Extraction Utilities

Extract and Exclude

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS';
 
// Keep only mutation methods
type MutationMethod = Extract<HttpMethod, 'POST' | 'PUT' | 'PATCH' | 'DELETE'>;
// 'POST' | 'PUT' | 'PATCH' | 'DELETE'
 
// Remove read-only methods
type WritableMethod = Exclude<HttpMethod, 'GET' | 'OPTIONS'>;
// 'POST' | 'PUT' | 'PATCH' | 'DELETE'
 
// Practical: filter event types
type EventName = 'user:created' | 'user:deleted' | 'order:created' | 'order:shipped';
type UserEvents = Extract<EventName, `user:${string}`>;
// 'user:created' | 'user:deleted'

NonNullable

// Remove null and undefined from a type
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>;  // User
 
// Common pattern with query results
async function requireUser(id: string): Promise<User> {
  const user = await db.users.findById(id);
  if (!user) throw new Error(`User ${id} not found`);
  return user; // NonNullable here — TypeScript knows it's not null
}

Function Introspection Utilities

async function createOrder(
  userId: string,
  items: Array<{ productId: string; quantity: number }>,
  shippingAddress: string
): Promise<{ orderId: string; total: number }> {
  // implementation
  return { orderId: crypto.randomUUID(), total: 0 };
}
 
// Infer the return type without importing it separately
type OrderResult = ReturnType<typeof createOrder>;         // Promise<{ orderId: string; total: number }>
type OrderResultData = Awaited<ReturnType<typeof createOrder>>; // { orderId: string; total: number }
 
// Infer parameter types
type OrderParams = Parameters<typeof createOrder>;
// [string, Array<{ productId: string; quantity: number }>, string]
 
// Extract class constructor parameters
class DatabasePool {
  constructor(
    public readonly url: string,
    public readonly maxConnections: number
  ) {}
}
 
type PoolArgs = ConstructorParameters<typeof DatabasePool>;  // [string, number]
type PoolInstance = InstanceType<typeof DatabasePool>;       // DatabasePool

String Manipulation Utilities

// Useful for code generation and mapped type keys
type EventName = 'userCreated' | 'orderShipped' | 'paymentFailed';
 
type UpperEvents = Uppercase<EventName>;
// 'USERCREATED' | 'ORDERSHIPPED' | 'PAYMENTFAILED'
 
// Create on/off handlers from event names
type EventHandlers<T extends string> = {
  [K in T as `on${Capitalize<K>}`]: () => void;
};
 
type Handlers = EventHandlers<'click' | 'submit' | 'focus'>;
// { onClick: () => void; onSubmit: () => void; onFocus: () => void }

Real-World Patterns

API Response Wrapper

type ApiSuccess<T> = { status: 'success'; data: T; timestamp: string };
type ApiError = { status: 'error'; message: string; code: string };
type ApiResponse<T> = ApiSuccess<T> | ApiError;
 
type PaginatedData<T> = {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
  hasNext: boolean;
};
 
type PaginatedResponse<T> = ApiResponse<PaginatedData<T>>;

Form and Validation State

interface OrderForm {
  customerId: string;
  items: string[];
  notes: string;
  rushDelivery: boolean;
}
 
type FormErrors = Partial<Record<keyof OrderForm, string>>;
type FormTouched = Partial<Record<keyof OrderForm, boolean>>;
 
interface FormState<T> {
  values: T;
  errors: Partial<Record<keyof T, string>>;
  touched: Partial<Record<keyof T, boolean>>;
  isSubmitting: boolean;
}

Common Mistakes

  • Using Partial<T> when Pick or Omit would be more precise and intention-revealing
  • Forgetting Awaited<> when extracting return types of async functions
  • Nesting Partial recursively by hand instead of writing a DeepPartial utility
  • Overusing Record<string, any> — prefer specific key unions for correctness
  • Ignoring NonNullable and using manual | null exclusions throughout the codebase

Best Practices

  • Derive types from a single source of truth — never duplicate interface definitions
  • Use Omit<T, 'sensitiveField'> on database entities before sending API responses
  • Apply Readonly<T> to configuration objects injected as dependencies
  • Prefer ReturnType<typeof fn> over manually typing function return shapes
  • Build a shared types/ module and import utilities rather than redeclaring them per file

Key Takeaways

  • Partial<T> makes all fields optional — perfect for PATCH request DTOs
  • Required<T> is the inverse of Partial and forces all optional fields to be present
  • Pick<T, Keys> and Omit<T, Keys> derive safe subsets without manual redeclaration
  • Record<K, V> creates typed lookup maps with exhaustive key checking
  • Extract<T, U> and Exclude<T, U> filter union members based on assignability
  • ReturnType<F> and Parameters<F> introspect function types at compile time
  • Awaited<T> unwraps nested Promise types introduced by async functions
  • Readonly<T> and custom DeepReadonly<T> prevent accidental mutation of configuration and domain objects

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading