open atlas
↑ К треку
Система типов TypeScript вглубь TS · 06 · 03

Функции-утверждения

Функции-утверждения (`asserts x is T`, `asserts cond`) сужают остаток области, выбрасывая исключение, — чем они отличаются от type-предикатов, почему аннотация возврата обязательна и какие ловушки ненадёжности есть.

TS Senior ◷ 14 min
Уровень
ОсновыJuniorMiddleSenior

Сервис читает 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. 1 До вызова `x` имеет свой широкий объявленный тип (например, unknown)
  2. 2 Функция-утверждение выполняется; если её утверждение ложно, она выбрасывает и управление покидает область
  3. 3 Если вызов вернулся нормально, ложный случай не произошёл
  4. 4 Компилятор сужает `x` до `string` начиная с оператора после вызова
  5. 5 Каждый последующий оператор в области видит сужённый тип без обрамляющей ветки
Вспомните перед уходом
  1. 01
    Объясните точно, как функция-утверждение меняет анализ потока управления и чем это отличается от type-предиката.
  2. 02
    Каковы две главные ловушки ненадёжности у функций-утверждений и как их смягчить?
Итог

Функции-утверждения (TS 3.7) — это возвращающие void функции с сигнатурой asserts x is T или asserts condition. Они сужают остаток области после вызова, выбрасывая исключение в ложном случае, что противоположно форме type-предиката: предикаты возвращают boolean и сужают только внутри истинной ветки, тогда как утверждения сужают на всю область, ведь ложный случай уже вышел. Правильное использование задают две ловушки ненадёжности: аннотация asserts обязательна и никогда не выводится (опустите — и сужение молча исчезнет), а тело никогда не сверяется с утверждением (утверждение, не выбросившее исключение, лжёт компилятору и крашится в рантайме). Держите их крошечными, очевидно-выбрасывающими и в идеале сгенерированными библиотекой. Дальше завершаем юнит оператором satisfies — он валидирует значение против типа, не расширяя его, дополняя касты и аннотации, которые мы использовали до сих пор. Теперь, когда встретишь лес ! или повторяющихся if (!x) throw для одной переменной, знаешь: их можно свернуть в один вызов assertDefined — и пусть утверждение сужает тип до конца функции.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 8 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources1
expand
  1. 01

Trademarks belong to their respective owners. Editorial reference only.