Данные извне
На таможне из первой темы tsc проверяет декларации посылок, которые идут через границу, — типы в вашем коде. Но часть посылок попадает в страну в обход границы: их привозят уже во время работы программы. Например:
- ответ сервера;
- строка, которую разбирает
JSON.parse; - содержимое файла.
У таких посылок нет декларации. tsc их не видел: когда он проверял программу, этих данных ещё не было.
Если к такой посылке самому написать декларацию, tsc поверит ей на слово:
type User = { name: string; age: number };
const user: User = JSON.parse('{"title": "report"}');
console.log(user.name.toUpperCase()); // ✗TypeError: Cannot read properties of undefined (reading 'toUpperCase')tsc молчит. JSON.parse возвращает any, а any подходит к любому типу, поэтому аннотация User проходит без вопросов. Но в строке нет поля name, и при запуске user.name оказывается undefined. Аннотация ничего не проверила: типы стёрты, и в JavaScript от User не осталось и следа.
Данные извне — в unknown
Безопасный порядок другой: всё, что пришло извне, кладут в переменную типа unknown. Тогда tsc не даст пользоваться данными без проверки:
const data: unknown = JSON.parse('{"title": "report"}');
console.log(data.title); // ✗error TS18046: 'data' is of type 'unknown'.Посылка без декларации остаётся опечатанной, пока её не вскроют. Зато ошибка видна до запуска, а не у пользователя.
Проверка предикатом типа
Проверять форму объекта прямо в каждом if неудобно. Проверку выносят в функцию с предикатом типа v is User: при запуске она смотрит на значение, а tsc по её ответу сужает тип:
type User = { name: string; age: number };
function isUser(v: unknown): v is User {
return typeof v === "object" && v !== null
&& "name" in v && typeof v.name === "string"
&& "age" in v && typeof v.age === "number";
}
const data: unknown = JSON.parse('{"name": "Anna", "age": 30}');
if (isUser(data)) {
console.log(data.name.toUpperCase()); // ANNA
} else {
console.log("not a user");
}Давайте разберём проверку по шагам:
typeof v === "object"отсекает числа, строки и другие примитивы.v !== nullнужен отдельно: в JavaScripttypeof nullтоже равно"object"."name" in vпроверяет, что поле есть, и после этогоtscразрешает читатьv.name.typeof v.name === "string"проверяет тип поля, а две последние проверки делают то же сage.- Внутри
if (isUser(data))уdataтипUser, и программа печатаетANNA.
В отличие от аннотации, эта проверка остаётся в JavaScript и работает при каждом запуске. Если сервер пришлёт другой объект, программа пойдёт в ветку else, а не упадёт.
Ответ сервера
С ответом сервера всё так же. Метод response.json() у ответа fetch возвращает Promise<any>: tsc не знает, что пришлёт сервер. Поэтому результат кладут в unknown и проверяют:
const data: unknown = await response.json();if (isUser(data)) { … }
Сама функция fetch не обобщённая: она всегда возвращает Promise<Response>, а запись fetch<User>(url) tsc не пропустит. Для больших объектов такие проверки обычно не пишут вручную: библиотеки вроде Zod описывают форму данных один раз и проверяют по ней значение при запуске.
На собеседовании часто спрашивают, почему TypeScript не может проверить ответ сервера. Ответ: типы стираются при компиляции, а ответ приходит при запуске, когда типов уже нет, поэтому проверять его должен код.
Здесь картинка с таможней перестаёт работать. Настоящего таможенника можно поставить и внутри страны, а tsc внутрь работающей программы не попадает. При запуске данные проверяет только ваш собственный код, такой как isUser.
Частая ошибка
В проверке забыли про null:
type User = { name: string };
function isUser(v: unknown): v is User {
return typeof v === "object" && "name" in v // ✗
&& typeof v.name === "string";
}error TS18047: 'v' is possibly 'null'.tsc напоминает, что typeof null === "object". После первой проверки v ещё может быть null, а "name" in null при запуске бросит TypeError. Добавьте v !== null сразу после typeof v === "object".
Главное
- Данные извне — ответ сервера,
JSON.parse, файл —tscне видел, и типы их не проверяют. - Аннотация
const user: User = JSON.parse(text)ничего не проверяет:JSON.parseвозвращаетany. - Данные извне кладут в
unknownи проверяют функцией с предикатомv is User, которая работает при запуске. response.json()возвращаетPromise<any>, аfetchне обобщённая, поэтому ответ сервера проверяет только ваш код.