Как на самом деле работают типы данных в js?
Это больше академический интерес, хочу разобраться как оно работает.
Это вы сейчас в очень глубокую нору заглядываете.
Движки стараются оптимизировать по максимуму, поэтому там очень много всего накручено помимо наличия int32. 
Я не удивлюсь если в вашем конкретно примере они сделают просто a=1.5 в обоих случаях и код будет идентичный до байта.
а потом эту a подставят куда надо и оптимизируют там еще что-то, например b=c+a превратится в b=c+1.5
кроме того, есть разные компиляторы - которые генерируют оптимальный код для частных случаев, или более "общий" но более медленный.
Например для каждой переменной во время работы записывается какой тип в нее приходит, если этот участок кода вызывается достаточно часто - он помечается "горячим" и затем смотрится, если у вас в "a" всегда один и тот же тип, то генерируется быстрый код для этого типа, и ставятся проверки, если вдруг придет что-то другое, то этот код выкидывается и происходит возврат к более общему.
И много чего другого, лишь бы быстрее работало.
Даже если вы прямо сейчас разберетесь во всем, то завтра запилят новую оптимизацию где-то в глубине движка и что-то будет работать уже по другому.
Схема в доке v8 говорит что у int всего 2 типа, int32 и unit32, а int8, int16, float32 etc. есть только у массивов.
Логично, что int не может быть float (=
В статье про оптимизацию говорилось верно, что Number до некоторого размера может храниться более оптимально. Кусочек кода, который дёргает парсер, когда встречает литерал:Handle<Object> Factory::NewNumber(double value, AllocationType allocation) {
  // Materialize as a SMI if possible.
  int32_t int_value;
  if (DoubleToSmiInteger(value, &int_value)) {
    return handle(Smi::FromInt(int_value), isolate());
  }
  return NewHeapNumber(value, allocation);
}Второй вариант более оптимальный, так как мы даем понять интерпретатору что нужно сразу выделить больше ячеек памяти, так как число с плавающей точкой. Насколько это правда?
Неправда потому, что значение иммутабельно в данном случае и будет выделение третьего куска памяти под результат. И одно лишь наличие точки не делает число не целым (что вполне объяснимо, ибо JS-юзер разницы не видит by design).
По коду сложения чисел видно, что для описаных вами случаев оба объекта приводятся к HeapNumber (aka "большой Number", но не путать c BigInt) и складываются, а ссылка начинает указывать на новый объект (даже в случае укороченной записи, ибо a += b <=> a = a + b).
Задействованная ветка:TF_BUILTIN(Add, AddStubAssembler) { // Операция сложения
  ....
  BIND(&if_left_smi);               // Левый операнд - "маленький" int
  {
    .....
    BIND(&if_right_heapobject);     // Правый операнд - HeapNumber
    {
      ....
      var_left_double.Bind(SmiToFloat64(left));
      var_right_double.Bind(LoadHeapNumberValue(right));
      Goto(&do_double_add);
      .....
    }  // if_right_heapobject
  }    // if_left_smi

  ....

  BIND(&do_double_add);
  {
    Node* value = Float64Add(var_left_double.value(), var_right_double.value());
    Return(AllocateHeapNumberWithValue(value));
  }
}