any против unknown: две двери из системы типов
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
У системы типов есть две двери наружу. За первой стоит any: компилятор перестаёт проверять и само значение, и всё, что из него получилось. За второй — unknown: внутрь пускают что угодно, а наружу не выпускают ничего, пока вы не докажете, что там лежит. Ширина у дверей одинаковая, разница — в замке.
Имя обманывает. any читается как «любой тип», а означает другое: «с этим значением проверок не будет». Компилятор не подбирает тип пошире — он снимает с себя обязательства.
Диагностик здесь ноль. Присваивание в count: number прошло молча, хотя внутри строка, — и дальше компилятор совершенно логично считал count числом: у чисел toFixed есть, придираться не к чему. Разницу нашёл движок, на четвёртой строке.
Обратите внимание на направление. В any можно положить что угодно — это половина беды. Вторая половина в том, что значение типа any можно положить куда угодно, и принимающая сторона при этом ничего не проверит.
any — это не тип значения, а разрешение не проверять. Ошибку он не убирает, а переносит из сборки в браузер.
Самое неприятное свойство any — он не сидит на месте. Всё, что получено из значения типа any, тоже становится any: свойство, элемент массива, результат вызова. Одна аннотация гасит проверки в цепочке произвольной длины.
Ни одной диагностики. Написано any в первой строке, а падает четвёртая — и между ними нет ни одного места, где компилятор мог бы возразить: у значения типа any любое свойство разрешено, и результат снова any.
Заражение работает и в обратную сторону: значение типа any проходит через любую границу, где его ждали с настоящим типом.
Аннотация n: number написана, но проверять было нечего: аргумент типа any подходит любому параметру. С number[] та же история — компилятор поверил, что в строке лежит массив чисел, и map разрешил.
Каждая строка с any — это не одна дыра, а воронка: она расширяется вниз по коду.
Явное any в коде видно сразу. Гораздо чаще он приходит сам: параметр без аннотации (это TS7006 из предыдущей статьи), catch в старых настройках, чужой пакет без типов — и JSON.parse, который по объявлению возвращает именно any.
Данные из сети, из хранилища, из чужого кода — это отдельная большая тема, ей посвящена целая глава про границы программы, и начинается она со статьи «Данные извне — это unknown». Пока достаточно запомнить, что граница — главный поставщик any в вашем проекте.
У unknown вход ровно такой же широкий: туда кладётся любое значение, и компилятор не возражает. Вся разница — на выходе.
Одна диагностика — и ровно там, где написана неправда: Тип "unknown" не может быть назначен для типа "number". Ниже компилятор молчит, потому что дальше всё честно: count объявлен числом, у числа есть toFixed. Ошибка поймана на границе, а не через три строки после неё.
Список того, что unknown запрещает, короткий — потому что запрещено почти всё.
Три диагностики, у всех один код и один текст: "value" относится к типу unknown. Это не «такого свойства нет» — компилятор не знает, есть оно там или нет, и поэтому не пропускает. Заметьте, что первые два вызова console.log всё-таки отработали: 2 и 421. Типы живут до запуска, а запуск идёт своим чередом.
| Что делаем со значением | any | unknown |
|---|---|---|
| Положить в него что угодно | можно | можно |
| Присвоить в переменную другого типа | можно | TS2322 |
Передать в параметр number | можно | TS2345 |
| Прочитать свойство, вызвать, сложить | можно | TS18046 |
Спросить typeof, сравнить через === | можно | можно |
Передать в console.log или в String() | можно | можно |
unknown — это «я пока не знаю, что здесь». any — «мне всё равно, что здесь». Первое честно и локально, второе заразно.
unknown — не тупик, а требование: докажи. Доказательством служит обычная проверка, которая работает в браузере; компилятор читает её и внутри ветки считает тип уже известным.
Строки третья и пятая одинаковые, а судьба у них разная: первая разрешена, вторая нет. Компилятор не «поверил на слово» — он проследил поток выполнения и понял, что внутрь if попадают только строки. Этот механизм называется сужением типа, и ему посвящена отдельная глава.
Для объектов и массивов доказательства чуть длиннее, зато привычные — те же проверки писались бы и в обычном JavaScript.
Ноль диагностик и два честных ответа. Обратите внимание: ни одной лишней строки ради компилятора здесь нет — raw !== null и "name" in raw нужны и на исполнении, иначе код упал бы на первом же неожиданном значении.
Есть и короткая дорога: сказать компилятору «поверь мне» через as. Доказательств он в этом случае не спрашивает.
Диагностик снова ноль, а падение осталось. as ничего не преобразует и не проверяет — он переписывает тип в глазах компилятора, и ответственность за правду переходит к вам. Отсюда простое правило: unknown плюс проверка — это защита, unknown плюс as — это тот же any, только длиннее.
Чаще всего unknown приходит к читателю сам — в catch. Под strict переменная ошибки объявлена именно так, и это не придирка: throw в JavaScript умеет бросать любое значение, не только Error.
До появления этой проверки переменная в catch имела тип any, и error.message проходило молча — ровно до того дня, когда кто-нибудь бросал строку и в логах появлялось undefined. Лечится всё тем же доказательством.
Практическое правило простое. Когда тип значения неизвестен — пишите unknown и разбирайтесь на месте. Это стоит нескольких строк проверок, зато проверки эти нужны и на исполнении: с непроверенными данными код падал бы и без всякого TypeScript.
any тоже не запрещён — у него есть честная роль: временная заплатка при переводе большого JavaScript-проекта на типы, обёртка над старым пакетом без описаний, кусок кода, до которого руки дойдут в четверг. Разница в том, что any — это долг, а не решение: он молчит сегодня, а счёт приходит в браузере пользователя.
TypeScript задумывался как надстройка над существующим JavaScript, а не как новый язык. Без any ни один большой проект нельзя было бы переводить на типы постепенно: пришлось бы описать всё и сразу, иначе не собирается. any — это дверь, через которую в систему типов заходит старый код.
Побочный эффект: дверь открывается и изнутри. unknown спокойно присваивается в any, и замок перестаёт работать — без единой диагностики.
Поэтому 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 и добейтесь, чтобы код снова компилировался без диагностик: длина печатается у строк и массивов, а для остального выводится внятное сообщение.
После замены на unknown строка value.length становится ошибкой TS18046 — компилятор требует доказательства. Годятся две проверки: typeof для строки и Array.isArray для массива. У обоих вариантов есть length типа number, поэтому объединять их в одном условии можно.
Проверки не «для компилятора»: без них третий вызов и в обычном JavaScript выдал бы бессмысленное undefined. Разница в том, что теперь их невозможно забыть.
Сообщение об ошибке
Под strict переменная в catch имеет тип unknown, поэтому error.message не проходит. Напишите describe так, чтобы диагностик не осталось, а на выходе всегда была осмысленная строка: для Error — его сообщение, для брошенной строки — она сама, для всего остального — хоть что-нибудь читаемое.
Два доказательства подряд, а в конце — запасной вариант, который работает с любым значением.
На двух бросках из условия выйдет текст от движка и неизвестная ошибка: [object Object]; добавьте третий, throw "сломалось", — получите саму строку. Ни одной диагностики и ни одного undefined в логах.
Граница вокруг JSON.parse
loadSettings отдаёт наружу any прямо из JSON.parse, и заражение уходит дальше по коду: умножение строки на число даёт NaN, а компилятор молчит. Поставьте на возврате unknown и добудьте число честно — а если его там нет, скажите об этом.
Достаточно одной аннотации возврата: unknown перекрывает заражению путь наружу, и все проверки собираются в одном месте — сразу за границей.
Выведется retries — не число: в JSON лежала строка. Такой разбор пишут не руками на каждом поле, а один раз функцией-проверкой — до них дойдём в главе про сужение типов.