Пользовательские защитники типов
Пользовательские защитники типов возвращают `x is T`. Проверяющий доверяет предикату несоундно — лгущий защитник компилируется и портит всё сужение ниже по коду.
В вашей общей библиотеке появляется хелпер isUser(x): x is User. Каждое место вызова, прошедшее его проверку, трактует значение как полностью типизированный User — читает .email, .roles, .id. Спустя месяцы — падение в проде: Cannot read properties of undefined (reading 'toLowerCase'). Тело защитника проверяло лишь typeof x === "object". Оно солгало. TypeScript ему поверил, сузил тысячи мест вызова до User и никого не предупредил. Предикат типа — это обещание, которое проверяющий принимает на веру, а нарушенное обещание невидимо до рантайма.
Что такое предикат типа
Когда вы выносите валидацию в хелпер, проверяющий теряет связь между булевым результатом и типом — и возвращает вам полное объединение обратно. Предикаты типа решают эту проблему: они позволяют переиспользуемой функции участвовать в той же машинерии сужения, что typeof и instanceof используют встроенно.
Обычная функция, возвращающая boolean, ничего не сужает для вызывающего — проверяющий не знает, что означает результат true. Аннотация возврата предиката типа, arg is T, говорит проверяющему: «когда вернётся true, считай arg типом T; когда false — вычти T».
function isString(x: unknown): x is string {
return typeof x === "string";
}
function handle(v: string | number) {
if (isString(v)) {
v;
// ^? string — предикат сузил, точь-в-точь как встроенная проверка
return v.toUpperCase();
}
v;
// ^? number — ветка false вычитает string
}Параметр предиката (x / именованный аргумент) должен быть одним из параметров функции или this. Тип возврата x is T заменяет обычный boolean — в рантайме функция всё равно просто возвращает булево значение.
Проверяющий доверяет вам несоундно
Это надо усвоить: TypeScript не проверяет, что тело предиката действительно доказывает заявленный тип. Аннотация is T — это ваше утверждение; проверяющий принимает его на веру. Защитник, чьё тело не соответствует его предикату, компилируется без ошибок:
type User = { id: string; email: string; roles: string[] };
// ЛГУЩИЙ защитник — тело проверяет куда меньше, чем заявляет предикат
function isUser(x: unknown): x is User {
return typeof x === "object" && x !== null;
}
const maybe: unknown = { id: "u1" }; // ни email, ни roles
if (isUser(maybe)) {
maybe.email.toLowerCase();
// ^? проверяющий думает, что maybe: User, email: string
// Рантайм: TypeError — email это undefined
}Никакой ошибки на этапе компиляции. Предикат — контракт, за соблюдение которого отвечаете только вы. Корректный защитник проверяет каждое поле, обещанное предикатом:
function isUser(x: unknown): x is User {
return (
typeof x === "object" && x !== null &&
"id" in x && typeof (x as Record<string, unknown>).id === "string" &&
"email" in x && typeof (x as Record<string, unknown>).email === "string" &&
"roles" in x && Array.isArray((x as Record<string, unknown>).roles)
);
}▸Почему это работает
Почему TypeScript вообще допускает эту несоундность? Потому что валидация в рантайме в общем случае неразрешима — проверяющий не может статически доказать, что произвольное булево выражение устанавливает структурный тип. Поэтому он проводит намеренную границу: проверяет ваше использование предиката (сужение), но делегирует корректность тела предиката вам. Это та же модель доверия, что и у as-утверждений, просто упакованная в переиспользуемую функцию. Дисциплина, которая это чинит: выводить защитники из единой схемы (Zod z.infer + safeParse, io-ts, Valibot), чтобы валидатор и тип не разошлись.
Защитники на основе in и различение без тега
Когда у членов нет общего литерального тега, проверка in внутри предиката сужает структурно:
type WithData = { data: string };
type WithError = { error: string };
function hasData(r: WithData | WithError): r is WithData {
return "data" in r;
}Это работает, но хрупко: если оба члена могли бы нести ключ data (пусть даже опционально), тест in больше не различает их. Когда вы владеете формами, предпочитайте явный тег-дискриминант — это следующий урок.
Защитники для массивов и записей
Частая задача — сузить unknown (например, JSON) до типизированного массива. Предикат должен проверить и контейнер, и выборку элементов — но проверка лишь одного элемента сама по себе несоундный срез:
function isStringArray(x: unknown): x is string[] {
return Array.isArray(x) && x.every((e) => typeof e === "string");
}
function isRecordOfNumbers(x: unknown): x is Record<string, number> {
return (
typeof x === "object" && x !== null && !Array.isArray(x) &&
Object.values(x).every((v) => typeof v === "number")
);
}x.every(...) по всему массиву честен; typeof x[0] === "string" (выборка одного элемента) — ложь, которой проверяющий всё равно верит. Стоимость every реальна для огромных массивов — именно поэтому разобрать-один-раз-на-границе (валидировать payload при входе в систему) лучше, чем перепроверять защитниками повсюду.
Предикат типа — как вышибала, ставящий браслет «проверенный User». Проверяющий никогда не перепроверяет браслет — поэтому если вышибала ___, все внутри типизированы неверно.
Взгляд вперёд: функции-утверждения
Близкий родственник бросает исключение вместо возврата булева значения. Функция-утверждение использует asserts x is T: если она вернулась нормально, проверяющий сужает x до T на остаток области видимости — без if:
function assertUser(x: unknown): asserts x is User {
if (!isUser(x)) throw new Error("not a User");
}
function f(x: unknown) {
assertUser(x);
x;
// ^? User — сужено отсюда и дальше, ведь управление продолжилось, только если не было броска
}Действует то же несоундное доверие плюс обязанность реально бросить на отрицательном случае. Функции-утверждения вы изучите глубже в юните 06 (functions-this).
Найди лгущий защитник
1/3Защитнику `isUser(x): x is User`, чьё тело проверяет лишь `typeof x === 'object'`, передан объект без обязательных полей. Что произойдёт?
Какое тело предиката честно доказывает `x is string[]`?
- 01Что делает аннотация возврата `x is T`, чего не делает обычный возврат `boolean`?
- 02Объясни, почему лгущий защитник типа опасен и как возникает несоундность.
- 03При структурном сужении почему предпочесть тег-дискриминант защитнику на `in` и каков безопасный паттерн защитника массива?
Пользовательские защитники типов распространяют машинерию сужения из прошлого урока на ваши собственные функции: аннотация возврата x is T заставляет проверяющего сузить аргумент до T на ветке true и вычесть его на ветке false. Ключевое свойство — это доверие несоундно: TypeScript никогда не проверяет, что тело предиката доказывает заявленный тип, поэтому неполный или лгущий защитник компилируется чисто и молча портит каждое место вызова, всплывая лишь как падение в рантайме. Честные защитники проверяют каждое обещанное поле, используют every по всему массиву, а не выборку, и предпочитают явные теги-дискриминанты тестам in, когда вы владеете формами; для продакшен-безопасности выводите защитники из схемы, чтобы валидатор и тип не разошлись. Бросающий вариант, asserts x is T, показан здесь и детально разбирается в юните 06. Далее размеченные объединения превращают хрупкое сужение через in/предикат в надёжный паттерн на тегах с проверкой исчерпания. Теперь, когда встречаешь защитник x is T на ревью, сразу проверяешь его тело поле за полем — потому что компилятор этого за тебя не сделает.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.