← все статьи
11 мин

TypeScript: паттерны, которые реально спасают

TypeScript – не просто "JavaScript с типами". Его система типов тьюринг-полная и позволяет кодировать сложные инварианты. Вот паттерны, которые мы используем постоянно – не ради красоты, а потому что они ловят ошибки до рантайма.

Discriminated Union: забудьте про boolean флаги

Классическая проблема: объект с кучей опциональных полей, которые зависят от состояния. Легко ошибиться и обратиться к полю, которого нет в данном состоянии.

// Плохо: компилятор не поможет
type ApiResponse = {
  isLoading: boolean;
  data?: User;
  error?: string;
}

// Хорошо: каждое состояние явно
type ApiResponse =
  | { status: 'loading' }
  | { status: 'success'; data: User }
  | { status: 'error'; error: string; code: number }

function render(response: ApiResponse) {
  switch (response.status) {
    case 'loading':
      return <Spinner />;
    case 'success':
      return <UserCard user={response.data} />;
    case 'error':
      return <Error msg={response.error} />;
  }
}

Branded Types: не перепутайте ID

userId и orderId – оба string. Компилятор не скажет, если вы передадите одно вместо другого. Branded types решают это без рантайм-оверхеда.

type UserId = string & { readonly _brand: 'UserId' };
type OrderId = string & { readonly _brand: 'OrderId' };

// Конструкторы: единственный способ создать значение
const UserId = (id: string): UserId => id as UserId;
const OrderId = (id: string): OrderId => id as OrderId;

function getOrder(id: OrderId): Promise<Order> {
  // ...
}

const uid = UserId('user-123');
const oid = OrderId('order-456');

getOrder(oid); // ok
getOrder(uid); // error: UserId is not assignable to OrderId

satisfies: валидация без потери вывода типа

Оператор satisfies (TS 4.9) позволяет проверить, что объект соответствует типу, не теряя при этом вывод конкретных литеральных типов.

type Config = {
  theme: 'light' | 'dark';
  lang: string;
  retries: number;
}

// С обычной аннотацией: config.theme имеет тип 'light' | 'dark'
const configAnnotated: Config = {
  theme: 'dark',
  lang: 'ru',
  retries: 3,
};

// С satisfies: config.theme имеет тип 'dark'
const config = {
  theme: 'dark',
  lang: 'ru',
  retries: 3,
} satisfies Config;

const bad = {
  theme: 'purple', // error: Type '"purple"' is not assignable
  lang: 'ru',
  retries: 3,
} satisfies Config;

infer в Conditional Types: извлекаем типы

infer позволяет "вытащить" часть типа внутри conditional type. Полезно для работы с промисами, массивами, функциями.

// Достать тип, который возвращает промис
type Awaited<T> = T extends Promise<infer R> ? R : T;

type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<User[]>>; // User[]
type C = Awaited<number>; // number

// Параметры функции
type Params<T extends (...args: any) => any> =
  T extends (...args: infer P) => any ? P : never;

type LoginParams = Params<typeof loginUser>;
// [email: string, password: string]

Template Literal Types: типы как строки

Генерация типов из строковых паттернов – мощный инструмент для event-driven систем и API-маршрутов.

type EventName = 'created' | 'updated' | 'deleted';
type Entity    = 'user' | 'order' | 'product';

type AppEvent = `${Entity}:${EventName}`;
// 'user:created' | 'user:updated' | 'user:deleted' |
// 'order:created' | ...

function on(event: AppEvent, handler: () => void): void {
  // ...
}

on('user:created', handler);  // ✓
on('user:banned',  handler);  // ✗ – такого события нет

← все статьи Следующая →Redis: больше чем кэш