Explain how Partial, Pick, Omit, and Record utility types work. When would you use conditional types?

6 minadvancedtypescriptpartialpickomitrecord

Quick Answer

TypeScript provides several built-in utility types that help transform existing types. These are essential for creating flexible and reusable type definitions.

Detailed Answer

TypeScript provides several built-in utility types that help transform existing types. These are essential for creating flexible and reusable type definitions.

Core Utility Types:

  1. Partial<T> - Makes all properties optional:
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

type PartialUser = Partial<User>;
// Equivalent to:
// {
//   id?: number;
//   name?: string;
//   email?: string;
//   age?: number;
// }

// Usage in update functions
function updateUser(id: number, updates: Partial<User>): void {
  // Only some fields need to be provided
  console.log(`Updating user ${id} with:`, updates);
}

updateUser(1, { name: 'John' }); // OK - only name provided
updateUser(1, { name: 'John', age: 30 }); // OK - multiple fields
  1. Pick<T, K> - Selects specific properties:
type UserSummary = Pick<User, 'id' | 'name'>;
// Equivalent to:
// {
//   id: number;
//   name: string;
// }

// Usage for API responses
function getUserSummary(id: number): UserSummary {
  const user = getUserById(id);
  return {
    id: user.id,
    name: user.name
  };
}
  1. Omit<T, K> - Excludes specific properties:
type UserWithoutId = Omit<User, 'id'>;
// Equivalent to:
// {
//   name: string;
//   email: string;
//   age: number;
// }

// Usage for creating new users (without ID)
function createUser(userData: Omit<User, 'id'>): User {
  return {
    id: generateId(),
    ...userData
  };
}
  1. Record<K, V> - Creates object type with specific keys and values:
type Status = 'loading' | 'success' | 'error';
type StatusMessages = Record<Status, string>;
// Equivalent to:
// {
//   loading: string;
//   success: string;
//   error: string;
// }

const messages: StatusMessages = {
  loading: 'Please wait...',
  success: 'Operation completed!',
  error: 'Something went wrong!'
};

// Dynamic object creation
type UserRoles = Record<string, string[]>;
const userRoles: UserRoles = {
  admin: ['read', 'write', 'delete'],
  user: ['read'],
  guest: []
};

Advanced Utility Types:

  1. Required<T> - Makes all properties required:
interface Config {
  apiUrl?: string;
  timeout?: number;
  retries?: number;
}

type RequiredConfig = Required<Config>;
// All properties are now required
  1. Readonly<T> - Makes all properties readonly:
type ReadonlyUser = Readonly<User>;
// All properties are now readonly
  1. Exclude<T, U> - Excludes types from union:
type AllColors = 'red' | 'green' | 'blue' | 'yellow';
type PrimaryColors = Exclude<AllColors, 'yellow'>; // 'red' | 'green' | 'blue'
  1. Extract<T, U> - Extracts types from union:
type MixedTypes = string | number | boolean | Date;
type PrimitiveTypes = Extract<MixedTypes, string | number | boolean>;
// 'string' | 'number' | 'boolean'

Custom Utility Types:

// Deep partial - makes nested properties optional too
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Non-nullable - removes null and undefined
type NonNullable<T> = T extends null | undefined ? never : T;

// Function properties only
type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

// Optional properties only
type OptionalPropertyNames<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

Conditional Types: Conditional types allow you to create types that depend on other types. They use the syntax T extends U ? X : Y.

When to Use Conditional Types:

  1. API Response Handling:
type ApiResponse<T> = T extends string 
  ? { message: T } 
  : { data: T };

type StringResponse = ApiResponse<string>; // { message: string }
type ObjectResponse = ApiResponse<User>; // { data: User }
  1. Function Overloading:
type OverloadedFunction<T> = T extends string
  ? (input: T) => string
  : T extends number
  ? (input: T) => number
  : (input: T) => T;

const process: OverloadedFunction<string> = (input) => input.toUpperCase();
const processNumber: OverloadedFunction<number> = (input) => input * 2;
  1. Array vs Non-Array Handling:
type Flatten<T> = T extends (infer U)[] ? U : T;

type StringArray = Flatten<string[]>; // string
type StringType = Flatten<string>; // string
  1. Infer Keyword - Extract Types:
// Extract return type from function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Extract parameter types
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

// Extract array element type
type ArrayElement<T> = T extends (infer U)[] ? U : never;

// Usage
type MyFunction = (a: string, b: number) => boolean;
type MyReturnType = ReturnType<MyFunction>; // boolean
type MyParameters = Parameters<MyFunction>; // [string, number]
  1. Recursive Conditional Types:
// Deep readonly
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// Deep required
type DeepRequired<T> = {
  [P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};

// JSON serializable types
type JSONValue = 
  | string 
  | number 
  | boolean 
  | null 
  | JSONValue[] 
  | { [key: string]: JSONValue };

type JSONSerializable<T> = T extends JSONValue ? T : never;

Practical Examples:

// Form handling with partial updates
interface UserForm {
  name: string;
  email: string;
  age: number;
  preferences: {
    theme: 'light' | 'dark';
    notifications: boolean;
  };
}

type UserFormUpdate = DeepPartial<UserForm>;

function updateUserForm(updates: UserFormUpdate): void {
  // Can update any nested property
  console.log('Updating form with:', updates);
}

updateUserForm({
  name: 'John',
  preferences: {
    theme: 'dark'
    // notifications can be omitted
  }
});

// Event handler types
type EventMap = {
  click: MouseEvent;
  keydown: KeyboardEvent;
  load: Event;
};

type EventHandler<T extends keyof EventMap> = (event: EventMap[T]) => void;

function addEventListener<T extends keyof EventMap>(
  event: T,
  handler: EventHandler<T>
): void {
  // Type-safe event handling
}

addEventListener('click', (event) => {
  // event is typed as MouseEvent
  console.log(event.clientX, event.clientY);
});

addEventListener('keydown', (event) => {
  // event is typed as KeyboardEvent
  console.log(event.key, event.code);
});

Best Practices:

  1. Use utility types to create variations of existing types
  2. Combine utility types for complex transformations
  3. Use conditional types for type-dependent logic
  4. Leverage infer to extract types from complex structures
  5. Create custom utility types for domain-specific needs
  6. Use conditional types sparingly - they can make code hard to understand