TypeScript Utility Types — Complete Reference

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

TypeScript provides a comprehensive set of utility types that transform types into new types. These are essential for reducing boilerplate and expressing complex type relationships clearly. Mastering utility types is a hallmark of advanced TypeScript developers.

Essential Utility Types

Partial and Required

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Make all properties optional
type PartialUser = Partial<User>;
const updateUser: PartialUser = { name: "Alice" }; // Valid

// Make all properties required
type RequiredUser = Required<PartialUser>;
const completeUser: RequiredUser = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
  age: 30,
}; // All required

Pick and Omit

// Select specific properties
type UserPreview = Pick<User, "id" | "name">;
const preview: UserPreview = { id: 1, name: "Alice" };

// Exclude specific properties
type UserWithoutEmail = Omit<User, "email">;
const user: UserWithoutEmail = {
  id: 1,
  name: "Alice",
  age: 30,
};

// Combining Pick and Omit for clarity
type PublicUser = Omit<User, "age">;
type AdminUser = User;

Record

// Create object with specific keys
type Permissions = Record<"read" | "write" | "delete", boolean>;
const adminPermissions: Permissions = {
  read: true,
  write: true,
  delete: true,
};

// Record with enum keys
enum Role {
  Admin = "admin",
  User = "user",
  Guest = "guest",
}

type RolePermissions = Record<Role, boolean>;
const permissions: RolePermissions = {
  [Role.Admin]: true,
  [Role.User]: true,
  [Role.Guest]: false,
};

// Record with union types
type Environment = "development" | "staging" | "production";
type ConfigByEnv = Record<Environment, { apiUrl: string }>;
const config: ConfigByEnv = {
  development: { apiUrl: "http://localhost:3000" },
  staging: { apiUrl: "https://staging.example.com" },
  production: { apiUrl: "https://api.example.com" },
};

Type Extraction Utilities

Extract, Exclude, and NonNullable

type Union = string | number | boolean;

// Extract matching types
type StringOrNumber = Extract<Union, string | number>; // string | number
type OnlyString = Extract<Union, string>; // string

// Exclude matching types
type NoString = Exclude<Union, string>; // number | boolean
type NoNumber = Exclude<Union, number | boolean>; // string

// Remove null and undefined
type Nullable = string | null | undefined;
type NonNullableString = NonNullable<Nullable>; // string

ReturnType and Parameters

function getUserById(id: number): Promise<User> {
  return Promise.resolve({ id, name: "", email: "", age: 0 });
}

type UserResponse = ReturnType<typeof getUserById>; // Promise<User>
type UserParams = Parameters<typeof getUserById>; // [number]

// Extract from constructor
class UserService {
  constructor(dbUrl: string, apiKey: string) {}
}

type ServiceParams = ConstructorParameters<typeof UserService>;
// [string, string]

type ServiceInstance = InstanceType<typeof UserService>; // UserService

String Manipulation Utilities

// Capitalize first character
type Capitalized = Capitalize<"hello">; // "Hello"

// Uncapitalize first character
type Uncapitalized = Uncapitalize<"Hello">; // "hello"

// Convert to uppercase
type Uppercase = Uppercase<"hello">; // "HELLO"

// Convert to lowercase
type Lowercase = Lowercase<"HELLO">; // "hello"

Advanced Utility Patterns

Readonly and Mutable

interface User {
  readonly id: number;
  name: string;
}

// Make all properties readonly
type ReadonlyUser = Readonly<User>;
// {
//   readonly id: number;
//   readonly name: string;
// }

// Remove readonly modifiers
type MutableUser = {
  -readonly [K in keyof User]: User[K];
};

// Practical: deep readonly for API responses
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

Getters and Setters

interface User {
  id: number;
  name: string;
  email: string;
}

// Create getter methods for all properties
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<User>;
// {
//   getId: () => number;
//   getName: () => string;
//   getEmail: () => string;
// }

// Create setter methods
type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

type UserSetters = Setters<User>;
// {
//   setId: (value: number) => void;
//   setName: (value: string) => void;
//   setEmail: (value: string) => void;
// }

Flatten Arrays

// Extract array element type
type Flatten<T> = T extends Array<infer U> ? U : T;

type NestedArray = [1, 2, [3, 4]];
type FlattenedArray = Flatten<NestedArray>; // 1 | 2 | [3, 4]

// Deep flatten
type DeepFlatten<T> = T extends Array<infer U>
  ? DeepFlatten<U>
  : T;

type DeepFlattened = DeepFlatten<[1, [2, [3, 4]]]>; // 1 | 2 | 3 | 4

Real-World Examples

API Response Wrapper

// Create consistent API responses
type ApiResponse<T> = {
  status: "success" | "error";
  data: T;
  timestamp: Date;
  errors?: string[];
};

type UserResponse = ApiResponse<User>;
type UsersResponse = ApiResponse<User[]>;

// Create paginated response
type PaginatedApiResponse<T> = ApiResponse<{
  items: T[];
  total: number;
  page: number;
  pageSize: number;
}>;

Form State Management

interface LoginForm {
  email: string;
  password: string;
  rememberMe: boolean;
}

// Form values
type FormValues = LoginForm;

// Form errors - optional fields that can be errors
type FormErrors = Partial<Record<keyof LoginForm, string>>;

// Form touched - track which fields user interacted with
type FormTouched = Partial<Record<keyof LoginForm, boolean>>;

interface FormState {
  values: FormValues;
  errors: FormErrors;
  touched: FormTouched;
}

Event System

interface EventMap {
  "user:created": { userId: number };
  "user:updated": { userId: number; changes: Partial<User> };
  "user:deleted": { userId: number };
}

type EventType = keyof EventMap;
type EventPayload<T extends EventType> = EventMap[T];

class EventEmitter {
  on<T extends EventType>(
    event: T,
    callback: (payload: EventPayload<T>) => void
  ): void {
    // Implementation
  }

  emit<T extends EventType>(event: T, payload: EventPayload<T>): void {
    // Implementation
  }
}

const emitter = new EventEmitter();
emitter.on("user:created", (payload) => {
  // payload: { userId: number }
  console.log(payload.userId);
});

Performance Tips

  1. Avoid complex nested conditionals - they slow compilation
  2. Use type over interface for utilities - interfaces have more overhead
  3. Cache computed types - TypeScript caches type definitions

FAQ

Q: What's the difference between Pick and Omit? A: Pick includes specific properties, Omit excludes them. Use whichever makes your intent clearer. For large interfaces with few exceptions, Omit is cleaner; otherwise, Pick is more explicit.

Q: Can I create custom utility types? A: Absolutely! Use mapped types and conditional types to build reusable utilities. Many popular libraries like type-fest provide additional utilities beyond the built-in ones.

Q: How does Record differ from an interface? A: Record is type-level and creates a type with specific keys. Interfaces are more flexible and better for documentation. Use Record for known key sets, interfaces for object shapes.


Utility types are the Swiss Army knife of TypeScript. They eliminate boilerplate, enforce consistency, and make complex type relationships explicit. Mastering them separates competent TypeScript developers from exceptional ones.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro