В соседнем посте в презентации ругали Тайпскрипт за то, что он “глючный”, показывая хитрый кусочек кода:

var invokes = [];
function test(template) {
  invokes.push(template);
}
function update(value) {
  test`some ${value}!`;
}
update(1);
update(2);
invokes[0] === invokes[1]; // ?

In @ECMAScript ? true
In @babeljs ? true
In @typescriptlang ? **false**

оригинальный твит

Я в первый раз столкнулся с такой записью шаблонов, поэтому стало интересно разобраться, что тут вообще происходит и действительно ли в Тайпскрипте ошибка.

Оказалось, что есть такой формат записи шаблонов: tag `string text ${expression} string text`, который вызовет функцию tag, передав ей структуру шаблона для ручной сборки. Иногда это действительно полезно. Спецификация гласит, что каждый вызов tag должен передавать один и тот же объект.

Поэтому ошибка в этом месте действительно есть. Она подтверждена, и ждёт исправления вот уже год: https://github.com/microsoft/TypeScript/issues/27460. Ну, думаю, ерундовая проблема, ща зашлю фикс. Стал разбираться, и выяснилось, что тут совсем не так всё просто, и сложность не в том, чтобы написать (правильный) код. И это не “вопрос компетенции”, как наругали разработчиков Тайпскрипта в Твиттере.

  • Во-первых, проблемы в Тайпскрипте нет, если этот код компилируется в модуль. Тогда этот кусок кода ведёт себя в точности как предписано стандартом, возвращая true.
  • Во-вторых, проблема при сборке в глобальной области видимости есть и в Бабеле, к поведению которого у авторов твита и видосика про глючность Тайпскрипта вопросов нет. Однако, он совсем не в порядке, и это как раз причина, по которой оно не исправлено в Тайпскрипте.

Чтобы было понятно, вот ключевое различие между Тайпскриптом и Бабелем:

Исходник:

function update(value) {
  test`some ${value}!`;
}

Бабель:

function _templateObject() {
  var data = _taggedTemplateLiteral(["some ", "!"]);

  _templateObject = function _templateObject() {
    return data;
  };

  return data;
}

function update(value) {
  test(_templateObject(), value);
}

Тайпскрипт же генерит вот такой код:

function update(value) {
  test(__makeTemplateObject(["some ", "!"], ["some ", "!"]), value);
}

Как видно по этому фрагменту, Бабель при первом вызове сохраняет значение шаблона, с тем, чтобы в последующих возвращать его же. Тайпскрипт же всегда будет создавать копию. Казалось бы - возьми, да и сделай так же, как в Бабеле.

Но я совсем не уверен, что код в Бабеле лучше. Напомню, что проблема есть только при генерации в глобальной области видимости. Что будет, если сгенерировать два .js с использованием шаблонов с тегами и подключить, например, в html?

Если эти js были собраны Бабелем, то работать оно не будет, так как случится конфликт имён: из двух соседних js в глобальный скоп прольются две функции _templateObject, они перекроются и первый шаблон тихо заменится вторым.

Если же исходники были собраны Тайпскриптом, то ошибки здесь не будет, код запустится и будет работать, но с нарушением спецификации - будет передаваться копия шаблона. Рядовому пользователю это навряд ли помешает, но может сломать библиотечный код.

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

Самый простой вариант обхода проблемы в Тайпскрипте - собрать тот же код в виде модуля, тогда создаётся один объект и проблема уходит:

function update(value) {
  test(templateObject_1 || (templateObject_1 = __makeTemplateObject(["some ", "!"], ["some ", "!"])), value);
}

Но как решить проблему в целом? Зацепиться тут не за что, кроме как за глобальную область видимости. А значит нужно каким-то образом генерировать уникальные имена для шаблонов в каждой единице трансляции. И кроме вариаций на тему глобального уникального идентификатора ничего на ум не приходит. Так и висит ошибка в статусе Needs Proposal - элегантного решения нет, а предлагать хак никто не торопится.