TypeScript Advanced Patterns: Utility Types and Mapped Types

TypeScript's advanced type system provides powerful tools for creating type-safe, maintainable code. Understanding utility types and mapped types is essential for advanced TypeScript development.

Utility Types

Partial

Makes all properties of T optional:

interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user";
}

type PartialUser = Partial<User>;
// Equivalent to:
// {
//   id?: number;
//   name?: string;
//   email?: string;
//   role?: 'admin' | 'user';
// }

function updateUser(id: number, updates: PartialUser): User {
  // Implementation
}

Required

Makes all properties of T required:

type StrictUser = Required<PartialUser>;
// All properties are now required again

Pick<T, K>

Creates a type by picking specific properties from T:

type UserCredentials = Pick<User, "email" | "role">;
// { email: string; role: 'admin' | 'user'; }

type PublicUser = Pick<User, "id" | "name">;
// { id: number; name: string; }

Omit<T, K>

Creates a type by omitting specific properties from T:

type UserWithoutId = Omit<User, "id">;
// { name: string; email: string; role: 'admin' | 'user'; }

Record<K, T>

Creates an object type with keys of type K and values of type T:

type UserRoles = Record<string, "admin" | "user" | "moderator">;

const roles: UserRoles = {
  "alice": "admin",
  "bob": "user",
  "charlie": "moderator",
};

Mapped Types

Basic Mapped Types

Transform existing types:

type ReadonlyUser = {
  readonly [P in keyof User]: User[P];
};

type OptionalUser = {
  [P in keyof User]?: User[P];
};

Advanced Mapped Types

Create complex type transformations:

type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]>
    : T[P];
};

Template Literal Types

Use template literals in types:

type EventName = "click" | "hover" | "focus";

type EventHandler<T extends EventName> = `on${Capitalize<T>}`;

type Handlers = {
  [K in EventName as EventHandler<K>]: () => void;
};
// {
//   onClick: () => void;
//   onHover: () => void;
//   onFocus: () => void;
// }

Conditional Types

Basic Conditional Types

Types that depend on conditions:

type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>; // 'yes'
type B = IsString<number>; // 'no'

Advanced Conditional Types

type Flatten<T> = T extends Array<infer U> ? U : T;

type A = Flatten<string[]>; // string
type B = Flatten<number>; // number

type FunctionReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type ReturnTypeOfParseInt = FunctionReturnType<typeof parseInt>; // number

Distributive Conditional Types

type ToArray<T> = T extends any ? T[] : never;

type A = ToArray<string | number>; // string[] | number[]

Advanced Patterns

Branded Types

Create nominal types for better type safety:

type UserId = string & { readonly __brand: "UserId" };
type PostId = string & { readonly __brand: "PostId" };

function createUserId(id: string): UserId {
  return id as UserId;
}

function getUser(id: UserId) {
  // TypeScript knows this is a UserId, not just any string
}

Discriminated Unions

Use discriminant properties for type safety:

type ApiResponse<T> =
  | { status: "success"; data: T }
  | { status: "error"; error: string };

function handleResponse<T>(response: ApiResponse<T>) {
  if (response.status === "success") {
    // TypeScript knows response.data exists and is of type T
    console.log(response.data);
  } else {
    // TypeScript knows response.error exists
    console.error(response.error);
  }
}

Function Overloads with Types

function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

const input = createElement("input"); // HTMLInputElement
const div = createElement("div"); // HTMLDivElement

Utility Type Combinations

API Response Types

type ApiEndpoint = {
  "/users": {
    GET: { response: User[] };
    POST: { body: Omit<User, "id">; response: User };
  };
  "/users/:id": {
    GET: { params: { id: string }; response: User };
    PUT: { params: { id: string }; body: Partial<User>; response: User };
    DELETE: { params: { id: string }; response: { success: boolean } };
  };
};

type ExtractResponse<
  Endpoint extends keyof ApiEndpoint,
  Method extends keyof ApiEndpoint[Endpoint],
> = ApiEndpoint[Endpoint][Method] extends { response: infer R } ? R : never;

type GetUsersResponse = ExtractResponse<"/users", "GET">; // User[]
type CreateUserResponse = ExtractResponse<"/users", "POST">; // User

Form Validation Types

type ValidationRule<T> = {
  required?: boolean;
  minLength?: T extends string ? number : never;
  maxLength?: T extends string ? number : never;
  pattern?: T extends string ? RegExp : never;
  custom?: (value: T) => boolean | string;
};

type ValidationSchema<T> = {
  [K in keyof T]: ValidationRule<T[K]>;
};

type UserForm = {
  name: string;
  email: string;
  age: number;
};

const userValidation: ValidationSchema<UserForm> = {
  name: { required: true, minLength: 2 },
  email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
  age: { required: true, custom: (age) => age >= 18 || "Must be 18+" },
};

Best Practices

Type Organization

// types/index.ts
export * from './user';
export * from './api';
export * from './validation';

// types/user.ts
export interface User { /* ... */ }
export type UserId = string & { readonly __brand: 'UserId' };

// types/api.ts
export type ApiResponse<T> = /* ... */;
export type ApiEndpoint = /* ... */;

Generic Constraints

type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;

type ButtonProps = ComponentProps<typeof Button>;

Type Assertions vs Type Guards

// Type guard (preferred)
function isUser(obj: any): obj is User {
  return obj && typeof obj.id === "number" && typeof obj.name === "string";
}

// Type assertion (use sparingly)
const user = someValue as User; // Can be unsafe

Conclusion

Advanced TypeScript patterns provide powerful tools for creating type-safe, maintainable code. Understanding utility types, mapped types, and conditional types allows you to express complex type relationships and catch errors at compile time rather than runtime.