Функции-утверждения
Функции-утверждения (`asserts x is T`, `asserts cond`) сужают остаток области, выбрасывая исключение, — чем они отличаются от type-предикатов, почему аннотация возврата обязательна и какие ловушки ненадёжности есть.
Сервис читает process.env.DATABASE_URL, который TypeScript типизирует как string | undefined. Каждый потребитель защищает его через if (!url) throw ... или рассыпает non-null-утверждения url!. И то и другое — шум. Хочется одного вызова assertDefined(url), после которого url — просто string до конца функции: ни ветки, ни !. Это функция-утверждение, и она меняет анализ потока управления так, как type-предикаты не могут.
Две формы сигнатуры, обе через asserts
Прежде чем тянуться к if (!x) throw или рассыпать ! по файлу, спросите себя: ложный случай — это баг или законный вариант? Если это баг, функция-утверждение выражает именно это намерение — а компилятор закрепляет его на весь остаток области.
В TypeScript 3.7 появились сигнатуры-утверждения: возвращающая void функция, чей тип возврата — asserts …. Есть две формы.
// Форма 1 — утверждает, что условие истинно
function assert(condition: unknown, msg?: string): asserts condition {
if (!condition) throw new Error(msg ?? "Assertion failed");
}
// Форма 2 — утверждает, что значение имеет конкретный тип
function assertIsString(x: unknown): asserts x is string {
if (typeof x !== "string") throw new TypeError("not a string");
}Контракт — это обещание компилятору: если эта функция вернулась нормально, утверждение выполнилось. Единственный способ «вернуться не нормально» — выбросить исключение. Поэтому после вызова компилятор может считать сужённый факт истинным для каждого последующего оператора в области, ведь будь факт ложным, управление до них не дошло бы.
function load(raw: unknown) {
assertIsString(raw);
raw;
// ^? string — сужён на остаток области, ветка не нужна
return raw.toUpperCase();
}Предикат против утверждения: сужение ветки против сужения области
В этом и состоит senior-различие. Type-предикат (x is T) возвращает boolean и сужает только внутри ветки, где boolean истинен. Функция-утверждение (asserts x is T) возвращает void и сужает остаток области после вызова, потому что ложный случай уже выбросил исключение.
// Предикат: сужает только внутри ветки if
function isStr(x: unknown): x is string {
return typeof x === "string";
}
function a(x: unknown) {
if (isStr(x)) {
x; // ^? string (только здесь)
}
x; // ^? unknown (вне ветки снова широкий)
}
// Утверждение: сужает всё после вызова
function b(x: unknown) {
assertIsString(x);
x; // ^? string (на весь оставшийся scope)
}Берите предикат, когда хотите обработать оба случая; берите утверждение, когда ложный случай — это баг, на котором надо упасть громко.
Аннотация обязательна — и не проверяется
Сигнатура-утверждение никогда не выводится. Если опустить явный тип возврата : asserts …, TypeScript типизирует функцию как возвращающую void, и вызов ничего не сужает — молча.
// Аннотация asserts отсутствует:
function assertNum(x: unknown) { // выведенный возврат: void
if (typeof x !== "number") throw new Error();
}
function c(x: unknown) {
assertNum(x);
x; // ^? unknown — НЕ сужён; эффект assert молча потерян
}Тут нет ошибки — лишь отсутствующее сужение, а это опасный вид бага. Хуже того, тело не проверяется против утверждения. TypeScript доверяет сигнатуре; он не проверяет, что функция действительно выбрасывает, когда утверждение ложно.
function brokenAssert(x: unknown): asserts x is string {
// забыли выбросить! ничего не делает
}
function d(x: unknown) {
brokenAssert(x);
x.toUpperCase(); // для компилятора x — `string`, но в рантайме чем угодно → краш
}В этом центральная ненадёжность: функция-утверждение — это несущее заявление, которое тайп-чекер принимает на веру. Тело должно действительно выбрасывать в ложном случае, иначе вы солгали компилятору. Держите функции-утверждения крошечными и очевидно-выбрасывающими, а генерацию доверяйте библиотекам валидации (Zod и др.), чтобы выброс не разъезжался с типом.
▸Почему это работает
Почему TypeScript не проверяет, что тело выбрасывает? Потому что доказать «эта функция выбрасывает на всех входах, где предикат ложен» в общем случае неразрешимо — это потребовало бы от компилятора вычислять произвольную рантайм-логику. У предикатов та же дыра: function isStr(x): x is string { return true; } компилируется и лжёт так же свободно. Система типов делегирует истинность заявлений о сужении вам; она проверяет лишь их форму. Поэтому функции-утверждения/предикаты принадлежат малому, проверенному ядру кодовой базы.
Утверждения и определённое присваивание
Функции-утверждения также взаимодействуют с анализом определённого присваивания. Частый паттерн — утверждать, что значение присвоено до использования, что позволяет компилятору считать let определённо-присвоенным после — но заметьте, это работает только через механизм asserts, а не через обычный поток управления, так что та же дисциплина «должна действительно выбросить» сохраняется. При надёжном использовании assertDefined(config) заменяет лес non-null-утверждений ! одной проверяемой точкой-перехватом.
В чём ключевое различие между type-предикатом `x is T` и сигнатурой-утверждением `asserts x is T`?
Расставьте, как анализ потока управления обрабатывает значение через вызов `asserts x is string`.
- 1 До вызова `x` имеет свой широкий объявленный тип (например, unknown)
- 2 Функция-утверждение выполняется; если её утверждение ложно, она выбрасывает и управление покидает область
- 3 Если вызов вернулся нормально, ложный случай не произошёл
- 4 Компилятор сужает `x` до `string` начиная с оператора после вызова
- 5 Каждый последующий оператор в области видит сужённый тип без обрамляющей ветки
- 01Объясните точно, как функция-утверждение меняет анализ потока управления и чем это отличается от type-предиката.
- 02Каковы две главные ловушки ненадёжности у функций-утверждений и как их смягчить?
Функции-утверждения (TS 3.7) — это возвращающие void функции с сигнатурой asserts x is T или asserts condition. Они сужают остаток области после вызова, выбрасывая исключение в ложном случае, что противоположно форме type-предиката: предикаты возвращают boolean и сужают только внутри истинной ветки, тогда как утверждения сужают на всю область, ведь ложный случай уже вышел. Правильное использование задают две ловушки ненадёжности: аннотация asserts обязательна и никогда не выводится (опустите — и сужение молча исчезнет), а тело никогда не сверяется с утверждением (утверждение, не выбросившее исключение, лжёт компилятору и крашится в рантайме). Держите их крошечными, очевидно-выбрасывающими и в идеале сгенерированными библиотекой. Дальше завершаем юнит оператором satisfies — он валидирует значение против типа, не расширяя его, дополняя касты и аннотации, которые мы использовали до сих пор. Теперь, когда встретишь лес ! или повторяющихся if (!x) throw для одной переменной, знаешь: их можно свернуть в один вызов assertDefined — и пусть утверждение сужает тип до конца функции.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.