Перейти к содержимому
Теория · Асинхронный код и данные извне · урок 7 из 10

Данные извне

На таможне из первой темы tsc проверяет декларации посылок, которые идут через границу, — типы в вашем коде. Но часть посылок попадает в страну в обход границы: их привозят уже во время работы программы. Например:

  • ответ сервера;
  • строка, которую разбирает JSON.parse;
  • содержимое файла.

У таких посылок нет декларации. tsc их не видел: когда он проверял программу, этих данных ещё не было.

Если к такой посылке самому написать декларацию, tsc поверит ей на слово:

TypeScript
type User = { name: string; age: number };

const user: User = JSON.parse('{"title": "report"}');
console.log(user.name.toUpperCase()); // ✗
TEXT
TypeError: Cannot read properties of undefined (reading 'toUpperCase')

tsc молчит. JSON.parse возвращает any, а any подходит к любому типу, поэтому аннотация User проходит без вопросов. Но в строке нет поля name, и при запуске user.name оказывается undefined. Аннотация ничего не проверила: типы стёрты, и в JavaScript от User не осталось и следа.

Данные извне — в unknown

Безопасный порядок другой: всё, что пришло извне, кладут в переменную типа unknown. Тогда tsc не даст пользоваться данными без проверки:

TypeScript
const data: unknown = JSON.parse('{"title": "report"}');
console.log(data.title); // ✗
TEXT
error TS18046: 'data' is of type 'unknown'.

Посылка без декларации остаётся опечатанной, пока её не вскроют. Зато ошибка видна до запуска, а не у пользователя.

Проверка предикатом типа

Проверять форму объекта прямо в каждом if неудобно. Проверку выносят в функцию с предикатом типа v is User: при запуске она смотрит на значение, а tsc по её ответу сужает тип:

TypeScript
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");
}

Давайте разберём проверку по шагам:

  1. typeof v === "object" отсекает числа, строки и другие примитивы.
  2. v !== null нужен отдельно: в JavaScript typeof null тоже равно "object".
  3. "name" in v проверяет, что поле есть, и после этого tsc разрешает читать v.name.
  4. typeof v.name === "string" проверяет тип поля, а две последние проверки делают то же с age.
  5. Внутри 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:

TypeScript
type User = { name: string };

function isUser(v: unknown): v is User {
    return typeof v === "object" && "name" in v // ✗
        && typeof v.name === "string";
}
TEXT
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 не обобщённая, поэтому ответ сервера проверяет только ваш код.