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); // ✗ – такого события нет