Короткий рабочий мост: модули, замыкания, неизменяемые преобразования, Promise, strict TypeScript, unknown, narrowing и discriminated unions.
Повторяем только то, что немедленно понадобится в компонентах, и сразу закрепляем это строгими типами.
Вы сможете принять неизвестные данные, проверить их на границе, преобразовать без мутаций и описать конечное множество состояний так, чтобы компилятор находил пропущенные ветви.
До начала достаточно уметь читать базовый JavaScript: переменные, функции, массивы и объекты. В конце должен появиться не конспект, а типизированный модуль нормализации данных с асинхронной границей и тестируемыми чистыми функциями.
Примитивы копируются как значения, а объекты и массивы передаются через ссылки на один объект. Spread создаёт только поверхностную копию: вложенный массив остаётся общим. React опирается на замену ссылок при обновлении состояния, поэтому входные данные и прежний state нельзя изменять. Используйте map, filter, object spread и точечное копирование каждого изменяемого уровня.
Следующий фрагмент показывает минимальный рабочий контракт, а не заготовку для слепого копирования.
type Item = { id: string; meta: { selected: boolean } };
function select(items: readonly Item[], id: string): Item[] {
return items.map(item =>
item.id === id
? { ...item, meta: { ...item.meta, selected: true } }
: item,
);
}readonly защищает контракт на уровне типов, а копирование meta не даёт новой версии разделить изменяемый вложенный объект со старой.
Рабочая проверка. Докажите тестом, что результат изменился, а исходный массив и вложенный meta остались прежними.
Типичная поломка. const copy = [...items]; copy[0].meta.selected = true меняет meta и в исходном массиве.
Функция замыкает переменные из лексической области, а не их исторические значения. Обработчик может видеть снимок конкретного рендера, потому что каждый вызов компонента создаёт новую область. ES-модуль вычисляется один раз на экземпляр module graph, поэтому изменяемая переменная верхнего уровня становится общей для всех потребителей и плохо подходит для пользовательского состояния.
Следующий фрагмент показывает минимальный рабочий контракт, а не заготовку для слепого копирования.
export function createSequence(start = 0) {
let current = start;
return () => {
current += 1;
return current;
};
}Состояние принадлежит экземпляру фабрики. Если current вынести на уровень модуля, независимые потребители неожиданно начнут делить счётчик.
Рабочая проверка. Создайте две последовательности и проверьте, что вызовы одной не меняют другую.
Типичная поломка. Хранить текущего пользователя или состояние формы в изменяемой переменной модуля.
async-функция всегда возвращает Promise. Продолжение после await выполняется как microtask, а не блокирует поток. Сам Promise не отменяется от потери интереса потребителя: нужно передать AbortSignal поддерживающему API либо игнорировать устаревший результат по явному поколению запроса. Ошибка до await и отклонение Promise сходятся в rejected Promise.
Следующий фрагмент показывает минимальный рабочий контракт, а не заготовку для слепого копирования.
async function loadProduct(id: string, signal: AbortSignal) {
const response = await fetch('/api/products/' + encodeURIComponent(id), { signal });
if (!response.ok) throw new Error('http_' + response.status);
return (await response.json()) as unknown;
}fetch принимает AbortSignal, но JSON всё равно остаётся unknown: успешный HTTP-статус не доказывает форму тела.
Рабочая проверка. Проверьте отклонение при HTTP 500 и отдельный путь AbortError без показа ложного сообщения об отказе.
Типичная поломка. Считать, что await блокирует браузер или автоматически отменяет предыдущий запрос.
unknown и проверка внешней границыДанные сети, localStorage и postMessage не становятся надёжными от generic-параметра клиента. Тип unknown запрещает использовать значение до доказательства. Type guard должен проверить каждое поле, на которое опирается доменная модель; typeof null === "object", поэтому null обрабатывается отдельно.
Следующий фрагмент показывает минимальный рабочий контракт, а не заготовку для слепого копирования.
type Product = { id: string; price: number };
function isProduct(value: unknown): value is Product {
if (typeof value !== 'object' || value === null) return false;
const row = value as Record<string, unknown>;
return typeof row.id === 'string' && Number.isFinite(row.price);
}Локальное приведение к Record<string, unknown> разрешает безопасно инспектировать ключи; наружу выходит только значение, прошедшее проверки.
Рабочая проверка. Добавьте случаи null, массива, NaN, пропущенного id и лишних полей.
Типичная поломка. Написать const product = await response.json() as Product и считать контракт проверенным.
Проверки typeof, in, равенство литералу и пользовательские guards сужают union по control flow. Для UI особенно полезен discriminated union: состояния idle, loading, success, failure получают разные обязательные поля. Так невозможно одновременно иметь loading: true, старую ошибку и отсутствующие данные без явного решения.
Следующий фрагмент показывает минимальный рабочий контракт, а не заготовку для слепого копирования.
type LoadState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'failure'; error: string };
function message<T>(state: LoadState<T>): string {
switch (state.status) {
case 'idle': return 'Не запускалось';
case 'loading': return 'Загрузка';
case 'success': return 'Готово';
case 'failure': return state.error;
}
}Дискриминант status связывает поля с состоянием. Несколько независимых boolean-флагов такой связи не выражают.
Рабочая проверка. Добавьте состояние refreshing со старыми данными и добейтесь ошибки компилятора во всех неполных switch.
Типичная поломка. Моделировать асинхронный UI тремя независимыми полями data?, error?, loading.
satisfies и сохранение вывода типовGeneric связывает несколько позиций типа, а не служит украшением функции. satisfies проверяет совместимость выражения с контрактом, сохраняя более точный выведенный тип самого выражения. В отличие от as, он не просит компилятор поверить разработчику и полезен для реестров маршрутов, вариантов и конфигураций.
Следующий фрагмент показывает минимальный рабочий контракт, а не заготовку для слепого копирования.
type Route = { path: string; auth: boolean };
const routes = {
home: { path: '/', auth: false },
account: { path: '/account', auth: true },
} satisfies Record<string, Route>;
type RouteName = keyof typeof routes;RouteName остаётся union конкретных ключей, а каждое значение одновременно проверяется по Route.
Рабочая проверка. Добавьте маршрут без auth и сравните диагностику satisfies, явной аннотации и assertion.
Типичная поломка. Использовать as Record<string, Route> и скрыть ошибку конфигурации.
Функциональный контракт включает параметры, возвращаемое значение и допустимость Promise. Тип () => void означает, что вызывающий игнорирует возвращаемое значение; он не превращает async-ошибки в обработанные. Для доменных команд полезнее явный Promise<Result> или синхронный result union. При включённом strictFunctionTypes нельзя безопасно подменять обработчик функцией, принимающей более узкий вход.
Рабочая проверка. Смоделируйте команду сохранения как () => Promise<SaveResult> и заставьте вызывающий код разобрать отказ.
Типичная поломка. Передать async callback в API, которое ожидает void, и потерять rejected Promise.
nevernever представляет невозможное значение после полного narrowing. Функция assertNever превращает добавление нового варианта union в ошибку компиляции там, где переход не обработан. Это особенно полезно в reducers и отображении конечных автоматов, но вызов должен находиться после корректного исчерпывающего switch.
Следующий фрагмент показывает минимальный рабочий контракт, а не заготовку для слепого копирования.
function assertNever(value: never): never {
throw new Error('Unhandled state: ' + JSON.stringify(value));
}Runtime-throw остаётся последней защитой от данных, пришедших в обход TypeScript, а параметр never обеспечивает compile-time сигнал.
Рабочая проверка. Добавьте событие reducer и убедитесь, что все переходы, селекторы и представления требуют обновления.
Типичная поломка. Поставить default: return state, скрыв новое событие без поведения.
Получите массив неизвестных записей каталога, провалидируйте форму каждой записи, нормализуйте цены и статусы, а затем верните discriminated union LoadResult. Запрещены any, мутация входа и утверждение as Product до runtime-проверки.
Критерий завершения: проверка типов отклоняет незаконные состояния, тесты доказывают неизменность входа и корректную обработку отказа Promise. Сначала зафиксируйте наблюдаемое поведение тестом или измерением, затем меняйте реализацию.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.
Далее: Инструментарий React-проекта