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 ; case 'success': return ; case 'error': return ; // TypeScript знает: в каждой ветке доступны только нужные поля } } 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 { ... } const uid = UserId('user-123'); const oid = OrderId('order-456'); getOrder(oid); // ✓ getOrder(uid); // ✗ Argument of type '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 config: 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', // ✗ Type '"purple"' is not assignable lang: 'ru', retries: 3 } satisfies Config; infer в Conditional Types: извлекаем типы
infer позволяет "вытащить" часть типа внутри conditional type. Полезно для работы с промисами, массивами, функциями.
// Достать тип, который возвращает промис type Awaited = T extends Promise ? R : T; type A = Awaited>; // string type B = Awaited>; // User[] type C = Awaited; // number // Параметры функции type Params any> = T extends (...args: infer P) => any ? P : never; type LoginParams = Params; // [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); // ✗ – такого события нет