Примитивы: string, number, boolean, bigint — и почему не String
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Аннотация примитива — самое короткое, что можно написать на TypeScript: двоеточие и одно слово. Слово это пишется строчными буквами — string, а не String. Разница не косметическая: заглавное имя означает совсем другой тип, и код с ним ломается там, где вы этого не ждёте.
Тип приписывается к имени через двоеточие, до знака равенства. Имена примитивных типов вы уже знаете наизусть — это ровно то, что возвращает typeof: string, number, boolean, bigint, symbol.
Компилятор сказал про шестую строку: Тип «string» не может быть назначен для типа «number». И всё равно выполнил её — типы стираются при сборке, до браузера доезжает обычный JavaScript. Последняя строка показывает, зачем вообще была нужна ругань: 199 + 1 дало бы 200, а "199" + 1 склеило строки, как в главе про преобразование типов.
Писать аннотацию каждый раз не нужно: если рядом стоит значение, тип берётся из него. Имя типа при этом получается то же самое, просто вы его не видите.
Аннотаций в примере нет, а ошибка на пятой строке та же: из 41 компилятор вывел number и запомнил это на всю жизнь переменной. Дальше — та самая расплата за проигнорированное предупреждение: у строки нет метода toFixed, и запуск падает с TypeError.
let даёт широкий тип (string), а const — узкий, состоящий из одного значения. Про такие типы — следующая статья главы.
Откуда вообще берётся вторая, заглавная String? Из части I: у примитива нет собственных свойств, но "текст".toUpperCase() работает, потому что движок на миг оборачивает примитив объектом, зовёт метод и тут же выбрасывает обёртку. Конструктор new String() делает эту временную обёртку постоянной — и получается объект, который лишь притворяется строкой.
Компилятор на этот код не сказал ни слова, а он весь состоит из ловушек: строгое сравнение сравнивает обёртку по ссылке и даёт false, а new Boolean(false) в условии ведёт себя как правда, потому что в условии проверяется объект, а не то, что внутри.
Так у каждого примитива образуется пара: строчное имя — сам примитив, заглавное — объект-обёртка вокруг него.
| Значение | typeof | Тип в TypeScript | Объект-обёртка |
|---|---|---|---|
"Дубай" | "string" | string | String |
199 | "number" | number | Number |
true | "boolean" | boolean | Boolean |
42n | "bigint" | bigint | BigInt |
Symbol("id") | "symbol" | symbol | Symbol |
Правый столбец в аннотациях не нужен никогда. Ни одного случая, где String лучше string, в обычном коде не существует — а поводов пожалеть хватает.
Первое, что удивляет: на заглавное имя компилятор не ругается сразу. Прежде чем запускать, решите, какая из двух строк примера ему не понравится.
Ошибка одна, и не там, где ждёшь: Тип «String» не может быть назначен для типа «string». «string» является примитивом, но «String» — объект оболочки. Предпочтительно использовать «string» по мере возможности. Компилятор прямым текстом подсказывает, что делать.
Асимметрия объясняется просто. У примитивной строки есть все методы, которые обещает тип String, — значит, примитив спокойно кладётся в переменную с заглавным типом, и первая строка проходит. Обратно не работает: объект-обёртка не является примитивом, и подсунуть его туда, где ждут string, нельзя.
Отсюда главная неприятность: String не сидит на месте, а расползается. Стоит написать его в сигнатуре функции — и он поедет дальше вместе с результатом.
Вызов прошёл, вычисление прошло, а ошибка вылезла в шестой строке — там, где результат попытались отдать нормальному коду. Внутри функции всё было в порядке; чинить придётся объявление, написанное совсем в другом месте.
Приписку «предпочтительно использовать...» компилятор добавляет к string, number, boolean и symbol. Для BigInt её нет — сообщение будет сухим.
Пока речь шла о ворчании компилятора, можно было отмахнуться. Хуже другое: тип String разрешает передать в функцию настоящую обёртку — и тогда ломаются проверки, написанные в части I.
Компилятор молчит на обоих вызовах: обёртка честно подходит под тип String. А typeof на исполнении видит объект и уводит второй вызов в ветку «это не строка». Ровно так же промахнутся JSON-проверки, ключи объектов и любой чужой код, который ждёт примитив.
Со строчным string такой ситуации не бывает: обёртка не пройдёт границу функции, и компилятор скажет об этом на месте вызова.
Можно и нужно: без new это не конструкторы, а функции преобразования, и возвращают они примитивы. Разница ровно в одном слове.
Последняя строка — про то, как язык учится на ошибках: у BigInt конструктора нет вовсе. Компилятор говорит Выражение new, у цели которого нет сигнатуры конструктора, неявно имеет тип any, а запуск падает с TypeError: BigInt is not a constructor.
number — это число с плавающей точкой, и целые в нём точны только до 2⁵³. Из главы про точность вы помните, чем это кончается для длинных идентификаторов и денег в копейках. bigint — отдельный примитив без этого потолка; литерал пишется с суффиксом n.
Компилятор здесь молчит — потеря разряда происходит в самом литерале, до всяких типов. Зато он не даёт смешать два числовых типа в одном выражении, и это как раз тот случай, когда предупреждение стоит читать.
Оператор «+» невозможно применить к типам «bigint» и «number» — и после диагностики код всё равно запускается, доходит до последней строки и падает с TypeError: Cannot mix BigInt and other types. Здесь компилятор не выдумывал строгость: запрет на смешивание придумал сам язык, TypeScript только сообщил о нём заранее.
bigint — только целые: 7n / 2n даёт 3n, остаток отбрасывается. Для дробей по-прежнему number.
- Имена примитивных типов пишутся строчными и совпадают с тем, что возвращает typeof: string, number, boolean, bigint, symbol.
- Заглавные String, Number, Boolean, Symbol — это типы объектов-обёрток; в аннотациях они не нужны никогда.
- Примитив назначается обёртке молча, а обёртка примитиву — нет, поэтому ошибка про String всплывает далеко от места, где её написали.
- Обёртка выживает на исполнении: typeof даёт "object", строгое сравнение возвращает false, а new Boolean(false) истинен в условии.
- String(), Number(), Boolean(), BigInt() без new — обычные преобразования и возвращают примитивы; с new получается объект (а у BigInt конструктора нет вовсе).
- bigint не смешивается с number: компилятор скажет об этом заранее, но запуск всё равно упадёт с TypeError.
1/4Что скажет компилятор на строку const city: String = "Дубай";?
2/4Чему равно typeof new String("Дубай") на исполнении?
3/4Переменная total имеет тип bigint, bonus — number. Что произойдёт при запуске console.log(total + bonus)?
4/4const paid = new Boolean(false); — что напечатает if (paid) с ветками «оплачен» и «ждём оплату»?
Строчными буквами
Функция makeSlug ждёт обычную строку, а объявления написаны заглавными именами. Найдите, на какую строку ругнётся компилятор, и почините аннотации так, чтобы диагностик не осталось.
Ругань одна — на вызове: Аргумент типа «String» нельзя назначить параметру типа «string». Объявления сами по себе законны, ошибка вылезает там, где значение пересекает границу функции. Лечится заменой трёх заглавных букв на строчные.
Аннотации здесь вообще можно убрать — вызов пройдёт и без них. Только типы получатся не эти: у const с примитивом выводятся литеральные "Sunset Cruise", 199 и false, каждый из одного значения. В string, number и boolean они укладываются, поэтому makeSlug доволен.
Оплата, которой не было
Код печатает «заказ оплачен», хотя оплаты не было, и компилятор молчит. Объясните себе, почему, и перепишите так, чтобы вывод стал честным.
new Boolean(false) — это объект-обёртка, а не false. В условии проверяется сам объект, а все объекты истинны, поэтому выполняется первая ветка. Убираем конструктор — и остаётся обычный примитив.
Если значение приходит извне строкой или числом, преобразуйте его функцией без new: Boolean("") даёт false, а new Boolean("") — истинный объект.
Сложить bigint с number
Функция должна прибавлять бонус к сумме, но не компилируется и падает на запуске. Почините тело функции, не меняя сигнатуру: base обязан остаться bigint, extra — number.
Складывать bigint с number запрещает сам язык, поэтому мало убедить компилятор — нужно настоящее преобразование. Функция BigInt() (без new) делает из числа большое целое.
Обратное преобразование Number(big) тоже есть, но оно теряет разряды выше 2⁵³ — для денег и идентификаторов лучше не выходить из bigint.