Примитивы в TS vs реальность V8

TypeScript показывает string, number, boolean как "примитивные типы". С точки зрения спецификации ECMAScript и реализации V8 — примитивных типов в JS не существует. Есть primitive value (значение, не тип) и оптимизации рантайма. Это важно для понимания, почему TS-типы не дают рантайм-гарантий.

Что это / Зачем в TS

Несоответствие модели TS и реальности рантайма:

  • В TS number — это монолитный тип.
  • В V8 number может быть SMI (31 бит на стеке), heap number, или объект-обёртка.
  • TS-проверка typeof x === 'string' не отражает того, что строки в V8 — обычные объекты в куче с property length, Symbol.iterator и т.д.

Что говорит спецификация ECMAScript

В разделе 6 ECMAScript описано 7 Language Types: undefined, null, boolean, number, bigint, symbol, string, object. Слова "примитив" в описании типов нет. Есть только понятие primitive value — значение, которое тип может вернуть.

"Они называются примитивами не потому что это тип, а потому что ниже них уже ничего нет."

Что происходит в V8

const s = 'abc';
s.length;        // как у строки берётся .length?

На самом деле:

  1. Доступ к property примитива → внутренняя операция ToObject.
  2. ToObject вызывает new String(s) под капотом.
  3. На обёртке вызывается length.

"На уровне V8 строка — обычный объект в куче со своими property."

Числа до 31 бита (SMI) — оптимизация: V8 кладёт их прямо в указатель, объект не создаётся. Это оптимизация, а не "примитивный тип".

Почему это важно для TS

function process(x: string) {
  x.length; // TS считает это property типа string
  // На самом деле — это property от new String(x), а не у самого x
}

Следствия:

  • Можно переопределить String.prototype[Symbol.iterator] — и [...'abc'] вернёт не символы.
  • Можно сломать valueOf/toString/Symbol.toPrimitive у объекта так, что он будет вести себя как примитив.
  • TS не видит этих манипуляций — никакая статическая типизация их не обнаружит.

Объект может притворяться примитивом

const num = {
  [Symbol.toPrimitive](hint: string) {
    if (hint === 'number') return 42;
    if (hint === 'string') return '42';
    return true;
  },
};

num + 1;     // 43 (number)
`${num}`;    // '42' (string)
num == true; // true

TS-тип этого объекта будет странным, но рантайм-поведение полностью идентично "примитиву".

Когда использовать / Когда НЕ

  • ✅ когда: понимаешь, почему TS-типы — это только подсказка IDE, а не рантайм-гарантия
  • ✅ когда: дебажишь странности typeof/instanceof
  • ✅ когда: пишешь библиотеки с Symbol.toPrimitive или Proxy
  • ❌ когда: бизнес-логика — здесь хватает TS-модели

Альтернативы

  • branded types — компенсируют отсутствие nominal typing.
  • рантайм-валидация (Zod/Joi) — единственная реальная гарантия типа на границе.
  • assert-функции — превращают TS-нарушение в throw в рантайме.

🎓 Источники

  • 🎓 Как одни фантазируют на тему типов в JavaScript · AsForJS · 2021-10-10 · YouTube
    • Тезисы: примитивных типов нет в спецификации; есть primitive value; все типы в V8 представлены объектами в куче.
    • Позиция автора: "В JS примитивных типов не существует."
  • 🎓 Я тип простой — я говорю стихами · AsForJS · 2023-03-29 · YouTube
    • Тезисы: только string и symbol — настоящие примитивы по критерию (формируют ключ); индекс массива всегда строка; SMI как оптимизация V8.
  • 🎓 Существует ли приведение типа в JavaScript · AsForJS · 2025-02-16 · YouTube

Противоречие с автором

автор обычно говорит про "формы объектов" в V8, не углубляясь в спор о терминологии. AsForJS жёстко спорит с термином "примитивный тип". На практике обе позиции не противоречат: TS-аннотации — это static check, а V8 видит всё как объекты с разными оптимизациями.

См. также