Самая распространенная ошибка в проектировании моделей данных — создание объектов с десятком опциональных булевых полей: { isLoading: boolean; isError: boolean; data?: User; error?: string }. Такая структура порождает недопустимые комбинации (например, когда isLoading: true и одновременно isError: true). Инженерный стандарт TypeScript — концепция Make Impossible States Impossible с использованием Discriminated Unions, исчерпывающей проверки Exhaustive Checking с типом never и кастомных предикатов типов (Type Guards).
isLoading: true и data: user, разработчик вынужден писать сложные вложенные тернарные операторы isLoading ? (data ? ... : ...) : ..., в которых неизбежно допускаются логические ошибки.CryptoPayment в систему, компилятор никак не предупредит разработчика о том, что в 12 файлах в конструкциях switch забыта обработка этого способа, что приведет к падению в рантайме.as Type:(data as AdminUser).permissions.Компилятор TypeScript непрерывно анализирует поток выполнения кода:
Входной тип: NetworkState (Объединение 4 состояний)
│
▼
switch (state.status) { ... }
│
┌──────────────┬────────────┴─────────────┬──────────────┐
▼ ▼ ▼ ▼
case 'idle': case 'loading': case 'success': case 'error':
│ │ │ │
state сужен state сужен state сужен state сужен
до IdleState до LoadingState до SuccessState до ErrorState
│ │ │ │
│ │ state.data state.error
│ │ ДОСТУПНО! ДОСТУПНО!
│ │ (Тип: User) (Тип: Error)
└──────────────┴────────────┬─────────────┴──────────────┘
│
default:
│
▼
assertNever(state)
(Если все ветки обработаны — тип: never. Ошибок нет.
Если добавили новый статус и забыли case — ОШИБКА КОМПИЛЯЦИИ!)Создание интерфейсов Idle, Loading, Success, Failure с единым строковым полем-дискриминатором status.
Объединение вариантов через type RequestState<T> = Idle | Loading | Success<T> | Failure.
Создание утилиты проверки типа never для гарантии обработки 100% кейсов в switch-блоках.
Создание функций isSuccessState(state): state is SuccessState для быстрого сужения в функциональных цепочках.
Использование синтаксиса asserts condition для валидации инвариантов и выброса понятных бизнес-ошибок.
У вас есть функция assertNever(value: never): never { throw new Error('Unhandled: ' + JSON.stringify(value)); }. Разработчик добавил в объединение статусов новый вариант status: 'cancelled', но забыл добавить case 'cancelled' в switch. Что произойдет при компиляции проекта?
Функция принимает широкий тип объединения (например, PaymentMethod = Card | Crypto | Cash).
Компилятор запрещает прямое обращение к полям cardNumber или cryptoWallet, так как они есть не во всех ветвях.
Оператор switch(payment.type) производит автоматическое разделение типов по литеральному полю.
Анализатор потока управления TypeScript моментально сужает тип внутри каждого блока case.
Для сложной вложенной логики предикат if (isCryptoPayment(payment)) сужает тип в произвольных выражениях.
Идеально подходит для фильтрации массивов: array.filter(isCryptoPayment).
Вызов assertAuthorized(user) сужает тип переменной user до конца текущей области видимости без создания блоков if/else.
Код после вызова гарантированно оперирует авторизованным пользователем.
/**
* Исчерпывающая утилита проверки полноты покрытия объединений.
* Вызывает ошибку компиляции, если в switch осталась необработанная ветвь.
*/
export function assertNever(value: never, message?: string): never {
throw new Error(message ?? `Необработанное состояние: ${JSON.stringify(value)}`);
}
// 1. Атомарные варианты состояния
export interface IdleState {
readonly status: 'idle';
}
export interface LoadingState {
readonly status: 'loading';
readonly startedAt: number;
}
export interface SuccessState<T> {
readonly status: 'success';
readonly data: T;
readonly receivedAt: number;
}
export interface ErrorState {
readonly status: 'error';
readonly error: Error;
}
// 2. Размеченное объединение (Discriminated Union)
export type AsyncState<T> = IdleState | LoadingState | SuccessState<T> | ErrorState;
// 3. Пользовательские предикаты типов (Type Guards)
export function isSuccessState<T>(state: AsyncState<T>): state is SuccessState<T> {
return state.status === 'success';
}
export function isErrorState<T>(state: AsyncState<T>): state is ErrorState {
return state.status === 'error';
}
export function isLoadingState<T>(state: AsyncState<T>): state is LoadingState {
return state.status === 'loading';
}import { assertNever } from '@/types/async-state';
export interface CardPayment {
readonly type: 'card';
readonly cardNumber: string;
readonly cvv: string;
}
export interface CryptoPayment {
readonly type: 'crypto';
readonly walletAddress: string;
readonly network: 'ethereum' | 'bitcoin' | 'polygon';
}
export interface BankTransferPayment {
readonly type: 'bank_transfer';
readonly iban: string;
readonly bic: string;
}
export type PaymentMethod = CardPayment | CryptoPayment | BankTransferPayment;
/**
* Пользовательский Type Guard
*/
export function isCryptoPayment(payment: PaymentMethod): payment is CryptoPayment {
return payment.type === 'crypto';
}
/**
* Функция утверждения (Assertion Function).
* Сужает тип payment до CardPayment для всех последующих строк в функции.
*/
export function assertCardPayment(payment: PaymentMethod): asserts payment is CardPayment {
if (payment.type !== 'card') {
throw new Error(`Ожидался платеж по карте, но получен: ${payment.type}`);
}
}
/**
* Обработчик транзакции со 100% гарантией исчерпывающего сопоставления
*/
export function formatPaymentSummary(payment: PaymentMethod): string {
switch (payment.type) {
case 'card':
return `Банковская карта **** ${payment.cardNumber.slice(-4)}`;
case 'crypto':
return `Криптокошелек (${payment.network}): ${payment.walletAddress.slice(0, 6)}...`;
case 'bank_transfer':
return `Банковский перевод IBAN: ${payment.iban}`;
default:
// При добавлении нового типа в PaymentMethod компилятор выдаст ошибку здесь
return assertNever(payment);
}
}Создание одного гигантского типа type Order = { type: 'card' | 'crypto'; cardNumber?: string; wallet?: string } — грубый антипаттерн. В нем поле cardNumber всегда имеет тип string | undefined, даже когда type === 'card'. Разделяйте варианты на изолированные интерфейсы и объединяйте их через type Order = CardOrder | CryptoOrder.
Если написать function isUser(val: any): boolean, компилятор вернет обычный boolean и НЕ сузит тип переменной в блоке if. Возвращаемый тип обязательно должен использовать ключевое слово is: function isUser(val: unknown): val is User.
TypeScript требует обязательного явного указания типа возвращаемого значения для assertion functions: const assertValid: (x: any) => asserts x is string = .... Иначе компилятор не сможет корректно применить эффект суждения к вызывающей области видимости.
У вас есть массив `items: (string | null | undefined)[]`. Разработчик пишет код: `const validItems = items.filter(x => Boolean(x));`. Какой тип будет иметь массив validItems в строгом режиме TypeScript?