Данные извне — это unknown

границы≈ 11 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

Компилятор проверяет ваш код, а не чужие данные. Ответ сервера, запись в хранилище, параметр из адреса страницы — про них известно ровно то, что написано в типах, и ни буквой больше. Беда в том, что стандартная библиотека обещает про них any: с первой же строки разбора проверки выключаются, и ошибка всплывает не в редакторе, а у пользователя.

Разделы темы:·

Самая частая дверь — JSON.parse. В стандартной библиотеке она объявлена как parse(text: string, reviver?): any, и это не оплошность: что лежит в строке, до разбора не знает никто.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ни одной диагностики — и TypeError на последней строке. Про any вы уже знаете из главы про две двери из системы типов: он не «любой тип», а «не проверяй». Здесь важно другое — его никто не выбирал. Он приехал вместе с данными.

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

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Строка восьмая проходит без звука, одиннадцатая получает TS2345: Аргумент типа "unknown" нельзя назначить параметру типа "Point". Данные в обеих строках одинаковые — разный только тип, которым мы их пометили. И обратите внимание на вывод: рисуем в NaN напечаталось дважды. Диагностика не отменяет выполнение, точно так же ведёт себя tsc по умолчанию.

ИсточникЧто обещает типОткуда это
JSON.parse(text)anyтак объявлено в стандартной библиотеке
await response.json()anyу ответа fetch метод объявлен как json(): Promise<any>
localStorage.getItem(key)string | nullтип строки честный, а про её содержимое он молчит
params.get(name) у URLSearchParamsstring | nullто же самое: тип есть, смысла нет
formData.get(name) у FormDataFormDataEntryValue | nullтип честный, но FormDataEntryValue — это File | string: вместо строки может приехать файл (разбор — в части IV, «FormData — это не объект со строками»)
event.data у события messageanyMessageEvent<T = any>: отправитель за пределами вкладки
переменная в catchunknownединственная граница, которую strict закрыл сам

Строка ответа приходит одинаково честной и от своего сервера, и от чужого: типы не пересекают сеть.

Заменить any на unknown в этой сигнатуре предлагают с 2018 года, и предложение до сих пор открыто. Причина простая: одной строкой в стандартной библиотеке перестали бы собираться почти все существующие проекты — везде, где результат разбора сразу используют как объект.

Так что any здесь — не рекомендация, а совместимость со старым кодом. Ставить границу приходится вручную, зато ровно там, где вы решите.

Компилятор не поставит unknown сам — сигнатуру он читает буквально. Аннотацию пишет автор, и это единственное место, где решается, откуда начинается проверяемый код.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

TS18046: "data" относится к типу unknown. Это не запрет, а вопрос: докажи, что там объект с полем user. Как отвечать на такой вопрос, разбиралось в главе про сужение — typeof, in, Array.isArray.

Только не путайте аннотацию с проверкой. Аннотация объявляет намерение, а данные остаются какими были.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Диагностик ноль: any подходит и типу number. В переменной port лежит строка "8080", о чём читатель узнаёт из падения. Достаточно поменять тип возврата у readConfig — и то же самое место становится видимым.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

TS2571: Объект имеет тип "Неизвестный" — формулировка неожиданная, компилятор перевёл на русский само слово unknown. Код диагностики другой, потому что жаловаться приходится не на именованную переменную, а на выражение. Падение при этом никуда не делось: типы стираются, до браузера доезжает обычный JavaScript.

Аннотация unknown ничего не стоит на исполнении — в собранном коде её просто нет.

unknown можно проверять там, где значение понадобилось. Тогда проверки расползаются: одно и то же поле доказывается в трёх функциях, и каждая знает про данные чуть-чуть. Дешевле разобрать один раз — на входе.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

В этой функции важны три вещи. Первая — сигнатура: внутрь unknown, наружу User. Она и есть граница, всё за ней написано в типах. Вторая — текст ошибки называет поле, а не «данные некорректны»: разбирать такое падение в логах придётся вам же. Третья — возвращается новый объект, а не проверенный raw: поле admin, которое сервер прислал заодно, дальше не поедет.

Проверять стоит то, что используете, а не весь ответ. Если из огромного ответа нужны два поля — доказывайте два поля: лишние проверки ломаются при каждой правке на сервере и ничего при этом не защищают.

Функция с типом raw is User из главы про сужение решает похожую задачу, но иначе: она пропускает исходное значение как есть, и компилятор верит ей на слово — тело предиката никто не проверяет. Парсер отдаёт собранный объект, и его возврат компилятор проверяет обычным образом: забудете поле — не соберётся.

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

С массивом легко решить, что дело сделано: проверка написана, ветка своя. Попробуйте предсказать, что скажет компилятор здесь.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Диагностик снова ноль — и снова падение. Array.isArray объявлен как isArray(arg: any): arg is any[], поэтому из unknown он сужает к any[]: сам массив доказан, а его элементы стали тем самым any, от которого мы уходили. Одна проверка — и замок открыт.

Лечится одной строкой: вернуть элементам unknown и пропустить каждый через тот же разбор.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

С вложенными объектами компилятор ведёт себя лучше: после "address" in raw поле raw.address имеет тип unknown, а не any. То есть спуск на второй уровень — это ровно та же работа, что и на первом, и её видно в типах.

Бросить исключение — не единственный ответ. Выбор зависит от того, можно ли работать дальше без этих данных. Настройки из хранилища, например, ронять приложение не должны: там лежит строка, которую записала версия трёхмесячной давности.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ни одного исключения и ни одной проверки на стороне вызывающего: readSettings всегда возвращает Settings. Мусор молча заменяется умолчанием — и заметьте, что "18" строкой не превратилось в число само собой, потому что превращать его никто не просил.

Когда данные нужны по-настоящему — бросайте, но ловите на границе, а не в глубине кода.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Обе записи пришли из одного источника, а судьба у них разная: первая проехала дальше в типизированный код, вторая остановилась на входе с внятным текстом. Третий вариант ответа — вернуть исход значением вида { ok: true, value } | { ok: false, error }: тогда вызывающий обязан посмотреть, что там, и try не нужен.

Ручной разбор хорош, пока полей немного. Дальше берут библиотеку схем — zod, valibot и подобные: форму описывают один раз, тип выводится из схемы, а проверка получается в комплекте, вместе с накопленным списком того, что именно не сошлось.

Платить приходится зависимостью и весом в сборке, поэтому для двух-трёх полей честнее свой парсер. Принцип от библиотеки не меняется: наружу — unknown, внутрь типизированного кода — только через разбор. А какую библиотеку схем берут для браузера и какую — для контракта, общего с сервером, напомнит полка браузера.

  • Данные из-за границы программы приходят с типом any: так объявлены JSON.parse, response.json() и event.data — компилятор перестаёт проверять всё, что из них получилось.
  • Границу ставит автор: аннотация unknown на переменной, параметре или типе возврата — единственное место, где вход перестаёт быть дырой.
  • Аннотация не проверяет значение, а объявляет намерение; проверяют только настоящие проверки, которые работают и в чистом JavaScript.
  • Разбор на входе лучше проверок по месту: функция вида (raw: unknown) => User отдаёт готовый тип, называет виноватое поле и отсекает лишнее.
  • Array.isArray сужает unknown до any[] — элементам нужно вернуть unknown, иначе проверка массива только создаёт впечатление защиты.
  • Что делать с плохими данными — решает место: бросить, подставить умолчание или вернуть исход значением; молча продолжать с any — не вариант.

1/4В коде const data = JSON.parse(body);, дальше data.user.age.toFixed(0), а поля age в ответе нет. Что покажет запуск?

2/4Переменная raw имеет тип unknown. Внутри if (Array.isArray(raw)) написано raw[0].name.toUpperCase(). Что скажет компилятор?

3/4Парсер заканчивается строкой return { name: raw.name, age: raw.age };. Сервер прислал ещё и поле admin. Что окажется в результате?

4/4Данные проходили через parseUser и всё работало. Со вторника сервер стал присылать age строкой. Что произойдёт?

Разбор задачи на входе

Напишите parseTask: она принимает unknown, возвращает Task, а на непохожих данных бросает TypeError с текстом, из которого видно, какое поле подвело. Заглушку уберите — сейчас она пропускает всё подряд.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Проверки идут сверху вниз: сначала что это вообще объект, потом каждое поле. Возвращается новый объект — так в Task попадает только описанное.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Проверка raw === null не формальность: typeof null тоже даёт "object", и без неё разбор упал бы на первом же null из ответа.

Массив с браком

В ответе четыре записи, годных из них две. Ронять всё из-за одной испорченной нельзя: соберите годные, остальные пропустите. Понадобятся две функции — toTask возвращает Task или null, collect собирает массив.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ключевая строка — const items: unknown[] = raw;. После Array.isArray элементы имеют тип any, и без неё toTask вызывался бы для значений, которые компилятор считает уже проверенными.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Цикл можно свернуть в одну строку: return items.map(toTask).filter((task) => task !== null); — компилятор сам выводит из такой стрелочной функции предикат и убирает null из типа результата.

Город на второй глубине

Город лежит внутри address, и адреса в ответе может не оказаться вовсе. Доберитесь до address.city, а на любом обрыве пути бросьте TypeError с указанием поля.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Вложенный объект — такая же граница, как и внешний: после "address" in raw поле приходит с типом unknown, и его приходится доказывать заново.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Если полей на втором уровне несколько, разбор выносят в отдельную функцию parseAddress(raw: unknown): Address — и получается ровно тот же приём, применённый на этаж ниже.