any против unknown: две двери из системы типов

базовые типы≈ 9 минут

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

У системы типов есть две двери наружу. За первой стоит any: компилятор перестаёт проверять и само значение, и всё, что из него получилось. За второй — unknown: внутрь пускают что угодно, а наружу не выпускают ничего, пока вы не докажете, что там лежит. Ширина у дверей одинаковая, разница — в замке.

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

Имя обманывает. any читается как «любой тип», а означает другое: «с этим значением проверок не будет». Компилятор не подбирает тип пошире — он снимает с себя обязательства.

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

Диагностик здесь ноль. Присваивание в count: number прошло молча, хотя внутри строка, — и дальше компилятор совершенно логично считал count числом: у чисел toFixed есть, придираться не к чему. Разницу нашёл движок, на четвёртой строке.

Обратите внимание на направление. В any можно положить что угодно — это половина беды. Вторая половина в том, что значение типа any можно положить куда угодно, и принимающая сторона при этом ничего не проверит.

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

any — это не тип значения, а разрешение не проверять. Ошибку он не убирает, а переносит из сборки в браузер.

Самое неприятное свойство any — он не сидит на месте. Всё, что получено из значения типа any, тоже становится any: свойство, элемент массива, результат вызова. Одна аннотация гасит проверки в цепочке произвольной длины.

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

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

Заражение работает и в обратную сторону: значение типа any проходит через любую границу, где его ждали с настоящим типом.

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

Аннотация n: number написана, но проверять было нечего: аргумент типа any подходит любому параметру. С number[] та же история — компилятор поверил, что в строке лежит массив чисел, и map разрешил.

Каждая строка с any — это не одна дыра, а воронка: она расширяется вниз по коду.

Явное any в коде видно сразу. Гораздо чаще он приходит сам: параметр без аннотации (это TS7006 из предыдущей статьи), catch в старых настройках, чужой пакет без типов — и JSON.parse, который по объявлению возвращает именно any.

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

Данные из сети, из хранилища, из чужого кода — это отдельная большая тема, ей посвящена целая глава про границы программы, и начинается она со статьи «Данные извне — это unknown». Пока достаточно запомнить, что граница — главный поставщик any в вашем проекте.

У unknown вход ровно такой же широкий: туда кладётся любое значение, и компилятор не возражает. Вся разница — на выходе.

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

Одна диагностика — и ровно там, где написана неправда: Тип "unknown" не может быть назначен для типа "number". Ниже компилятор молчит, потому что дальше всё честно: count объявлен числом, у числа есть toFixed. Ошибка поймана на границе, а не через три строки после неё.

Список того, что unknown запрещает, короткий — потому что запрещено почти всё.

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

Три диагностики, у всех один код и один текст: "value" относится к типу unknown. Это не «такого свойства нет» — компилятор не знает, есть оно там или нет, и поэтому не пропускает. Заметьте, что первые два вызова console.log всё-таки отработали: 2 и 421. Типы живут до запуска, а запуск идёт своим чередом.

Что делаем со значениемanyunknown
Положить в него что угодноможноможно
Присвоить в переменную другого типаможноTS2322
Передать в параметр numberможноTS2345
Прочитать свойство, вызвать, сложитьможноTS18046
Спросить typeof, сравнить через ===можноможно
Передать в console.log или в String()можноможно

unknown — это «я пока не знаю, что здесь». any — «мне всё равно, что здесь». Первое честно и локально, второе заразно.

unknown — не тупик, а требование: докажи. Доказательством служит обычная проверка, которая работает в браузере; компилятор читает её и внутри ветки считает тип уже известным.

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

Строки третья и пятая одинаковые, а судьба у них разная: первая разрешена, вторая нет. Компилятор не «поверил на слово» — он проследил поток выполнения и понял, что внутрь if попадают только строки. Этот механизм называется сужением типа, и ему посвящена отдельная глава.

Для объектов и массивов доказательства чуть длиннее, зато привычные — те же проверки писались бы и в обычном JavaScript.

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

Ноль диагностик и два честных ответа. Обратите внимание: ни одной лишней строки ради компилятора здесь нет — raw !== null и "name" in raw нужны и на исполнении, иначе код упал бы на первом же неожиданном значении.

Есть и короткая дорога: сказать компилятору «поверь мне» через as. Доказательств он в этом случае не спрашивает.

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

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

Чаще всего unknown приходит к читателю сам — в catch. Под strict переменная ошибки объявлена именно так, и это не придирка: throw в JavaScript умеет бросать любое значение, не только Error.

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

До появления этой проверки переменная в catch имела тип any, и error.message проходило молча — ровно до того дня, когда кто-нибудь бросал строку и в логах появлялось undefined. Лечится всё тем же доказательством.

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

Практическое правило простое. Когда тип значения неизвестен — пишите unknown и разбирайтесь на месте. Это стоит нескольких строк проверок, зато проверки эти нужны и на исполнении: с непроверенными данными код падал бы и без всякого TypeScript.

any тоже не запрещён — у него есть честная роль: временная заплатка при переводе большого JavaScript-проекта на типы, обёртка над старым пакетом без описаний, кусок кода, до которого руки дойдут в четверг. Разница в том, что any — это долг, а не решение: он молчит сегодня, а счёт приходит в браузере пользователя.

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

Побочный эффект: дверь открывается и изнутри. unknown спокойно присваивается в any, и замок перестаёт работать — без единой диагностики.

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

Поэтому any в чужом коде ищут глазами, а в своём — линтером: у typescript-eslint для этого есть отдельные правила, о них разговор в главе про настройку проекта.

  • any — не «любой тип», а выключенная проверка: значение такого типа кладётся куда угодно, и принимающая сторона молчит.
  • any заразен: свойство, элемент и результат вызова тоже становятся any, поэтому падение случается далеко от строки, где он написан.
  • unknown принимает любое значение так же охотно, но наружу не выпускает: чтение свойства, вызов и арифметика дают TS18046, присваивание — TS2322.
  • Разрешено то, что безопасно для любого значения: typeof, ===, String(), вывод в консоль. Отладке unknown не мешает.
  • Ключ к unknown — доказательство: typeof, instanceof, in, Array.isArray. Проверка нужна и на исполнении, так что это не работа ради компилятора.
  • as доказательством не является: он ничего не проверяет и превращает unknown обратно в any, только многословнее.

1/4Переменной value: any присвоили строку "42", затем написали const count: number = value и вызвали count.toFixed(1). Что произойдёт?

2/4В том же коде заменили any на unknown. Что изменится?

3/4Что можно сделать со значением типа unknown без единой проверки?

4/4Значение типа unknown содержит строку, но написано const n = raw as number. Что скажет компилятор?

Закрыть дверь

Функция принимает any и печатает длину. Для строки и массива ответ верный, для числа выходит тихое undefined: ошибки нет, но и ответа нет. Замените any на unknown и добейтесь, чтобы код снова компилировался без диагностик: длина печатается у строк и массивов, а для остального выводится внятное сообщение.

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

После замены на unknown строка value.length становится ошибкой TS18046 — компилятор требует доказательства. Годятся две проверки: typeof для строки и Array.isArray для массива. У обоих вариантов есть length типа number, поэтому объединять их в одном условии можно.

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

Проверки не «для компилятора»: без них третий вызов и в обычном JavaScript выдал бы бессмысленное undefined. Разница в том, что теперь их невозможно забыть.

Сообщение об ошибке

Под strict переменная в catch имеет тип unknown, поэтому error.message не проходит. Напишите describe так, чтобы диагностик не осталось, а на выходе всегда была осмысленная строка: для Error — его сообщение, для брошенной строки — она сама, для всего остального — хоть что-нибудь читаемое.

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

Два доказательства подряд, а в конце — запасной вариант, который работает с любым значением.

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

На двух бросках из условия выйдет текст от движка и неизвестная ошибка: [object Object]; добавьте третий, throw "сломалось", — получите саму строку. Ни одной диагностики и ни одного undefined в логах.

Граница вокруг JSON.parse

loadSettings отдаёт наружу any прямо из JSON.parse, и заражение уходит дальше по коду: умножение строки на число даёт NaN, а компилятор молчит. Поставьте на возврате unknown и добудьте число честно — а если его там нет, скажите об этом.

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

Достаточно одной аннотации возврата: unknown перекрывает заражению путь наружу, и все проверки собираются в одном месте — сразу за границей.

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

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