January 22

ECMAScript 6+ vs TypeScript

Минули те времена, когда разработчики писали Frontend на "чистом" JavaScript (вплоть до ECMAScript 5). Все изменилось с выходом в свет версии ECMAScript 6 в 2015-м году. Это событие стало, по истине значимым в мировой Frontend разработке. Предыдущие 6 лет до этого, язык практически не менялся. Годом ранее, в 2014-м, компания Microsoft опубликовала TypeScript 1.0 и предоставила встроенную поддержку языка в своей IDE VisualStudio 2013. На самом деле, официально, TypeScript был выпущен еще в 2012 (версия 0.8), однако, популярностью он не пользовался в виду практически полного отсутствия поддержки со стороны существующих, на тот момент, IDE.

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

Типизацию TypeScript рассматривать в этой статье не будем, т.к. очевидно, что в ECMAScript её нет, и сравнивать тут нечего.

Для чистоты эксперимента код будем транспилировать в старый добрый ECMAScript 5. TypeScript, для удобства, возьмем версии 4.8.4 (этой версии, для целей статьи достаточно) и будем компилировать его родным tsc компилятором. Для ECMAScript воспользуемся инструментарием Babel.

Объявление переменных

Начнем с самого простого, с переменных. В этой части, оба языка имеют идентичный синтаксис.

ECMAScript

let a = "";
const b = "";
var c = "";
// == babel ==
var a = "";
var b = "";
var c = "";

TypeScript

let a = "";
const b = "";
var c = "";
// == tsc ==
var a = "";
var b = "";
var c = "";

В обоих случаях, после транспиляции, переменные приводятся к единственному возможному варианту декларирования переменных в ECMAScript 5, посредством var.

Однако, мы знаем, что let и const отличаются от var тем, что область их видимости ограничена LexicalEnvironment.

Попробуем обернуть переменную let в простейший LexicalEnvironment - Block.

ECMAScript

{
  let a = "1";
}

a = "2";
// == babel ==
{
  var _a = "1"; // babel добавил префикс "_" к переменной, таким образом, 
                // отделив её от глобальной переменной "a" ниже
}
a = "2";

TypeScript

{
  let a = "1";
}

a = "2";
// == tsc ==
{
  var a = "1"; // tsc не переименовал переменную и никаким другим образом
               // не изолировал её от переопределения ниже.
}
a = "2";

Как видимо из примера, TypeScript никак не позаботился об изоляции переменной let в runtime. В этой части компилятор полностью полагается на compile time исключение. Значит ли это, что TypeScript не безопасен в runtime в принципе?

Давайте взглянем на следующий пример

ECMAScript

{
  let a = "1";
  
  setTimeout(() => {
    console.log(a);
  }, 0);
}

a = "2";
// == babel ==
{
  var _a = "1";
  setTimeout(function () {
    console.log(_a);
  }, 0);
}
a = "2";

С точки зрения ECMAScript переменная по прежнему "изолирована" и переопределения не произойдет.

TypeScript

{
  let a = "1";
  
  setTimeout(() => {
    console.log(a);
  }, 0);
}

a = "2";
// == tsc ==
{
  var a_1 = "1";    
  setTimeout(function () {
    console.log(a_1);
  }, 0);
}
a = "2";

В этом случае, TypeScript учел факт того, что к переменной есть обращение в макротаске и позаботился о её изоляции, аналогично ECMAScript. С точки зрения разработчика, изоляция обеспечена, однако вопрос злонамеренных действий из вне, всё ещё остается открыт. По крайней мере, это чуть менее безопасно, чем в случае с ECMAScript.

C let разобрались, давайте теперь посмотрим на const

ECMAScript

const b = "1";
b = "2";
// == babel ==
function _readOnlyError(name) {
  throw new TypeError('"' + name + '" is read-only');
}
var b = "1";
"2", _readOnlyError("b");

Babel не позволил переопределить константу и вернул исключение в runtime.

TypeScript

const b = "1";
b = "2";
// == tsc ==
const b = "1";
b = "2";

TypeScript, как и в случае с let, никак не позаботился о защите const в runtime, полагаясь на ошибку компиляции.

Стрелочные функции

Еще одним нововведением ES6, стали стрелочные функции. Они отличаются от обычных отсутствием собственного контекста и ссылки на arguments.

ECMAScript

const foo = () => {
  this.a = "1"
}
// == babel ==
var _this = this;
var foo = function foo() {
  _this.a = "1";
};

Здесь мы видим, что Babel заменил ссылку на контекст функции, перенаправив обращение в глобальный контекст.

TypeScript

const foo = () => {
  this.a = "1"
}
// == tsc ==
var _this = this;
var foo = function foo() {
  _this.a = "1";
};

Абсолютно идентичным образом поступил и tsc.

А как обстоят дела с массивом arguments функции?

ECMAScript

const foo = () => {
  console.log(arguments.length);
}
// == babel ==
var _arguments = typeof arguments === "undefined" ? void 0 : arguments;
var foo = function foo() {
  console.log(_arguments.length);
};

Babel предусмотрительно проверил, нет ли переменной с таким именем в глобальном контексте, и если вдруг есть, он вернет ссылку на него. В противном случае массив будет не определен.

TypeScript

const foo = () => {
  console.log(arguments.length);
}
// == tsc ==
var foo = function () {
  console.log(arguments.length);
};

В случае с TypeScript, ссылка на arguments будет вести на реальный массив аргументов функции, исключение случится только в compile time.

Классы

Одним из важнейших нововведение ES6 были классы. Взглянем на простейшую реализацию класса

ECMAScript

class A {}
// == babel ==
function _typeof(o) {
  "@babel/helpers - typeof";
  return (
    (_typeof =
      "function" == typeof Symbol && "symbol" == typeof Symbol.iterator
        ? function (o) {
            return typeof o;
          }
        : function (o) {
            return o &&
              "function" == typeof Symbol &&
              o.constructor === Symbol &&
              o !== Symbol.prototype
              ? "symbol"
              : typeof o;
          }),
    _typeof(o)
  );
}
function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
  }
}
function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  Object.defineProperty(Constructor, "prototype", { writable: false });
  return Constructor;
}
function _toPropertyKey(t) {
  var i = _toPrimitive(t, "string");
  return "symbol" == _typeof(i) ? i : String(i);
}
function _toPrimitive(t, r) {
  if ("object" != _typeof(t) || !t) return t;
  var e = t[Symbol.toPrimitive];
  if (void 0 !== e) {
    var i = e.call(t, r || "default");
    if ("object" != _typeof(i)) return i;
    throw new TypeError("@@toPrimitive must return a primitive value.");
  }
  return ("string" === r ? String : Number)(t);
}
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}
var A = /*#__PURE__*/ _createClass(function A() {
  _classCallCheck(this, A);
});

Простое объявление класса, после трансипляции средствами Babel, приводит к довольно громоздкому коду в виде набора фабрик, чтобы обеспечить необходимый уровень безопасности

TypeScript

class A {}
// == tsc ==
var A = /** @class */ (function () {
    function A() {
    }
    return A;
}());

Та же самая инструкция на TypeScript создает всего несколько строк кода.

Дополним наш класс полями и методами.

ECMAScript

class A {
  propA = "1";
  #probB = "2";
  
  methodC() {}
  
  #methodD() {}
}
// == babel ==
function _typeof(o) {
  "@babel/helpers - typeof";
  return (
    (_typeof =
      "function" == typeof Symbol && "symbol" == typeof Symbol.iterator
        ? function (o) {
            return typeof o;
          }
        : function (o) {
            return o &&
              "function" == typeof Symbol &&
              o.constructor === Symbol &&
              o !== Symbol.prototype
              ? "symbol"
              : typeof o;
          }),
    _typeof(o)
  );
}
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}
function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i];
    descriptor.enumerable = descriptor.enumerable || false;
    descriptor.configurable = true;
    if ("value" in descriptor) descriptor.writable = true;
    Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor);
  }
}
function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps);
  if (staticProps) _defineProperties(Constructor, staticProps);
  Object.defineProperty(Constructor, "prototype", { writable: false });
  return Constructor;
}
function _classPrivateMethodInitSpec(obj, privateSet) {
  _checkPrivateRedeclaration(obj, privateSet);
  privateSet.add(obj);
}
function _classPrivateFieldInitSpec(obj, privateMap, value) {
  _checkPrivateRedeclaration(obj, privateMap);
  privateMap.set(obj, value);
}
function _checkPrivateRedeclaration(obj, privateCollection) {
  if (privateCollection.has(obj)) {
    throw new TypeError(
      "Cannot initialize the same private elements twice on an object"
    );
  }
}
function _defineProperty(obj, key, value) {
  key = _toPropertyKey(key);
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}
function _toPropertyKey(t) {
  var i = _toPrimitive(t, "string");
  return "symbol" == _typeof(i) ? i : String(i);
}
function _toPrimitive(t, r) {
  if ("object" != _typeof(t) || !t) return t;
  var e = t[Symbol.toPrimitive];
  if (void 0 !== e) {
    var i = e.call(t, r || "default");
    if ("object" != _typeof(i)) return i;
    throw new TypeError("@@toPrimitive must return a primitive value.");
  }
  return ("string" === r ? String : Number)(t);
}
var _propB = /*#__PURE__*/ new WeakMap();
var _methodD = /*#__PURE__*/ new WeakSet();
var A = /*#__PURE__*/ (function () {
  function A() {
    _classCallCheck(this, A);
    _classPrivateMethodInitSpec(this, _methodD);
    _defineProperty(this, "propA", "1");
    _classPrivateFieldInitSpec(this, _propB, {
      writable: true,
      value: "2"
    });
  }
  _createClass(A, [
    {
      key: "methodC",
      value:
        // private

        function methodC() {}

      // private
    }
  ]);
  return A;
})();
function _methodD2() {}

В данном классе присутствуют публичное свойство, приватное свойство, публичный метод и приватный метод. Приватные свойства и метода Bebel поместил в Weak-коллекции. Публичныей же - настроил посредством Object.defineProperty.

TypeScript

class A {
  public propA = "1";
  protected propB = "2";
  private propC = "3";
  
  public methodD() {}
  
  protected methodE() {}
  
  private methodF() {}
}
// == tsc ==
var A = /** @class */ (function () {
    function A() {
        this.propA = "1";   
        this.propB = "2";    
        this.propC = "3";
    }
    A.prototype.methodD = function () { };
    A.prototype.methodE = function () { };
    A.prototype.methodF = function () { };
    return A;
}());

Не смотря на то, что TypeScript имеет чуть более гибкую и более традиционную объектную модель, итоговый код весьма примитивен. Свойства просто помещаются в функциональный контекст, а методы - в прототип. Никаких проверок и настроек свойств и методов тут не происходит, что позволяет нам в runtime, например, обратиться к protected свойству или вызвать private-метод из класса-наследника

var __extends = (this && this.__extends) || (function () {
  var extendStatics = function (d, b) {
      extendStatics = Object.setPrototypeOf ||
          ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
              function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
          return extendStatics(d, b);
  };
  return function (d, b) {
      if (typeof b !== "function" && b !== null)
          throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
      extendStatics(d, b);
      function __() { this.constructor = d; }
      d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
  };
})();
var A = /** @class */ (function () {
    function A() {
        this.propA = "1";
        this.propB = "2";
        this.propC = "3";
    }
    A.prototype.methodD = function () { };
    A.prototype.methodE = function () { };
    A.prototype.methodF = function () { };
    return A;
}());
var B = /** @class */ (function (_super) {
    __extends(B, _super);
    function B() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    B.prototype.methodG = function () {
        this.methodF();
    };
    return B;
}(A));

В примере выше, класс B, фактически, имеет доступ к приватному методу родителя через свою ссылку на контекст.

Await/Async

В версии ES7 появились асинхронные операторы await и async. Посмотрим, как они устроены.

ECMAScript

async function foo() {
  await Promise.resolve();
}
// == babel ==
function _typeof(o) {
  "@babel/helpers - typeof";
  return (
    (_typeof =
      "function" == typeof Symbol && "symbol" == typeof Symbol.iterator
        ? function (o) {
            return typeof o;
          }
        : function (o) {
            return o &&
              "function" == typeof Symbol &&
              o.constructor === Symbol &&
              o !== Symbol.prototype
              ? "symbol"
              : typeof o;
          }),
    _typeof(o)
  );
}
function _regeneratorRuntime() {
  "use strict";
  /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ _regeneratorRuntime =
    function _regeneratorRuntime() {
      return e;
    };
  var t,
    e = {},
    r = Object.prototype,
    n = r.hasOwnProperty,
    o =
      Object.defineProperty ||
      function (t, e, r) {
        t[e] = r.value;
      },
    i = "function" == typeof Symbol ? Symbol : {},
    a = i.iterator || "@@iterator",
    c = i.asyncIterator || "@@asyncIterator",
    u = i.toStringTag || "@@toStringTag";
  function define(t, e, r) {
    return (
      Object.defineProperty(t, e, {
        value: r,
        enumerable: !0,
        configurable: !0,
        writable: !0
      }),
      t[e]
    );
  }
  try {
    define({}, "");
  } catch (t) {
    define = function define(t, e, r) {
      return (t[e] = r);
    };
  }
  function wrap(t, e, r, n) {
    var i = e && e.prototype instanceof Generator ? e : Generator,
      a = Object.create(i.prototype),
      c = new Context(n || []);
    return o(a, "_invoke", { value: makeInvokeMethod(t, r, c) }), a;
  }
  function tryCatch(t, e, r) {
    try {
      return { type: "normal", arg: t.call(e, r) };
    } catch (t) {
      return { type: "throw", arg: t };
    }
  }
  e.wrap = wrap;
  var h = "suspendedStart",
    l = "suspendedYield",
    f = "executing",
    s = "completed",
    y = {};
  function Generator() {}
  function GeneratorFunction() {}
  function GeneratorFunctionPrototype() {}
  var p = {};
  define(p, a, function () {
    return this;
  });
  var d = Object.getPrototypeOf,
    v = d && d(d(values([])));
  v && v !== r && n.call(v, a) && (p = v);
  var g =
    (GeneratorFunctionPrototype.prototype =
    Generator.prototype =
      Object.create(p));
  function defineIteratorMethods(t) {
    ["next", "throw", "return"].forEach(function (e) {
      define(t, e, function (t) {
        return this._invoke(e, t);
      });
    });
  }
  function AsyncIterator(t, e) {
    function invoke(r, o, i, a) {
      var c = tryCatch(t[r], t, o);
      if ("throw" !== c.type) {
        var u = c.arg,
          h = u.value;
        return h && "object" == _typeof(h) && n.call(h, "__await")
          ? e.resolve(h.__await).then(
              function (t) {
                invoke("next", t, i, a);
              },
              function (t) {
                invoke("throw", t, i, a);
              }
            )
          : e.resolve(h).then(
              function (t) {
                (u.value = t), i(u);
              },
              function (t) {
                return invoke("throw", t, i, a);
              }
            );
      }
      a(c.arg);
    }
    var r;
    o(this, "_invoke", {
      value: function value(t, n) {
        function callInvokeWithMethodAndArg() {
          return new e(function (e, r) {
            invoke(t, n, e, r);
          });
        }
        return (r = r
          ? r.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg)
          : callInvokeWithMethodAndArg());
      }
    });
  }
  function makeInvokeMethod(e, r, n) {
    var o = h;
    return function (i, a) {
      if (o === f) throw new Error("Generator is already running");
      if (o === s) {
        if ("throw" === i) throw a;
        return { value: t, done: !0 };
      }
      for (n.method = i, n.arg = a; ; ) {
        var c = n.delegate;
        if (c) {
          var u = maybeInvokeDelegate(c, n);
          if (u) {
            if (u === y) continue;
            return u;
          }
        }
        if ("next" === n.method) n.sent = n._sent = n.arg;
        else if ("throw" === n.method) {
          if (o === h) throw ((o = s), n.arg);
          n.dispatchException(n.arg);
        } else "return" === n.method && n.abrupt("return", n.arg);
        o = f;
        var p = tryCatch(e, r, n);
        if ("normal" === p.type) {
          if (((o = n.done ? s : l), p.arg === y)) continue;
          return { value: p.arg, done: n.done };
        }
        "throw" === p.type && ((o = s), (n.method = "throw"), (n.arg = p.arg));
      }
    };
  }
  function maybeInvokeDelegate(e, r) {
    var n = r.method,
      o = e.iterator[n];
    if (o === t)
      return (
        (r.delegate = null),
        ("throw" === n &&
          e.iterator.return &&
          ((r.method = "return"),
          (r.arg = t),
          maybeInvokeDelegate(e, r),
          "throw" === r.method)) ||
          ("return" !== n &&
            ((r.method = "throw"),
            (r.arg = new TypeError(
              "The iterator does not provide a '" + n + "' method"
            )))),
        y
      );
    var i = tryCatch(o, e.iterator, r.arg);
    if ("throw" === i.type)
      return (r.method = "throw"), (r.arg = i.arg), (r.delegate = null), y;
    var a = i.arg;
    return a
      ? a.done
        ? ((r[e.resultName] = a.value),
          (r.next = e.nextLoc),
          "return" !== r.method && ((r.method = "next"), (r.arg = t)),
          (r.delegate = null),
          y)
        : a
      : ((r.method = "throw"),
        (r.arg = new TypeError("iterator result is not an object")),
        (r.delegate = null),
        y);
  }
  function pushTryEntry(t) {
    var e = { tryLoc: t[0] };
    1 in t && (e.catchLoc = t[1]),
      2 in t && ((e.finallyLoc = t[2]), (e.afterLoc = t[3])),
      this.tryEntries.push(e);
  }
  function resetTryEntry(t) {
    var e = t.completion || {};
    (e.type = "normal"), delete e.arg, (t.completion = e);
  }
  function Context(t) {
    (this.tryEntries = [{ tryLoc: "root" }]),
      t.forEach(pushTryEntry, this),
      this.reset(!0);
  }
  function values(e) {
    if (e || "" === e) {
      var r = e[a];
      if (r) return r.call(e);
      if ("function" == typeof e.next) return e;
      if (!isNaN(e.length)) {
        var o = -1,
          i = function next() {
            for (; ++o < e.length; )
              if (n.call(e, o))
                return (next.value = e[o]), (next.done = !1), next;
            return (next.value = t), (next.done = !0), next;
          };
        return (i.next = i);
      }
    }
    throw new TypeError(_typeof(e) + " is not iterable");
  }
  return (
    (GeneratorFunction.prototype = GeneratorFunctionPrototype),
    o(g, "constructor", {
      value: GeneratorFunctionPrototype,
      configurable: !0
    }),
    o(GeneratorFunctionPrototype, "constructor", {
      value: GeneratorFunction,
      configurable: !0
    }),
    (GeneratorFunction.displayName = define(
      GeneratorFunctionPrototype,
      u,
      "GeneratorFunction"
    )),
    (e.isGeneratorFunction = function (t) {
      var e = "function" == typeof t && t.constructor;
      return (
        !!e &&
        (e === GeneratorFunction ||
          "GeneratorFunction" === (e.displayName || e.name))
      );
    }),
    (e.mark = function (t) {
      return (
        Object.setPrototypeOf
          ? Object.setPrototypeOf(t, GeneratorFunctionPrototype)
          : ((t.__proto__ = GeneratorFunctionPrototype),
            define(t, u, "GeneratorFunction")),
        (t.prototype = Object.create(g)),
        t
      );
    }),
    (e.awrap = function (t) {
      return { __await: t };
    }),
    defineIteratorMethods(AsyncIterator.prototype),
    define(AsyncIterator.prototype, c, function () {
      return this;
    }),
    (e.AsyncIterator = AsyncIterator),
    (e.async = function (t, r, n, o, i) {
      void 0 === i && (i = Promise);
      var a = new AsyncIterator(wrap(t, r, n, o), i);
      return e.isGeneratorFunction(r)
        ? a
        : a.next().then(function (t) {
            return t.done ? t.value : a.next();
          });
    }),
    defineIteratorMethods(g),
    define(g, u, "Generator"),
    define(g, a, function () {
      return this;
    }),
    define(g, "toString", function () {
      return "[object Generator]";
    }),
    (e.keys = function (t) {
      var e = Object(t),
        r = [];
      for (var n in e) r.push(n);
      return (
        r.reverse(),
        function next() {
          for (; r.length; ) {
            var t = r.pop();
            if (t in e) return (next.value = t), (next.done = !1), next;
          }
          return (next.done = !0), next;
        }
      );
    }),
    (e.values = values),
    (Context.prototype = {
      constructor: Context,
      reset: function reset(e) {
        if (
          ((this.prev = 0),
          (this.next = 0),
          (this.sent = this._sent = t),
          (this.done = !1),
          (this.delegate = null),
          (this.method = "next"),
          (this.arg = t),
          this.tryEntries.forEach(resetTryEntry),
          !e)
        )
          for (var r in this)
            "t" === r.charAt(0) &&
              n.call(this, r) &&
              !isNaN(+r.slice(1)) &&
              (this[r] = t);
      },
      stop: function stop() {
        this.done = !0;
        var t = this.tryEntries[0].completion;
        if ("throw" === t.type) throw t.arg;
        return this.rval;
      },
      dispatchException: function dispatchException(e) {
        if (this.done) throw e;
        var r = this;
        function handle(n, o) {
          return (
            (a.type = "throw"),
            (a.arg = e),
            (r.next = n),
            o && ((r.method = "next"), (r.arg = t)),
            !!o
          );
        }
        for (var o = this.tryEntries.length - 1; o >= 0; --o) {
          var i = this.tryEntries[o],
            a = i.completion;
          if ("root" === i.tryLoc) return handle("end");
          if (i.tryLoc <= this.prev) {
            var c = n.call(i, "catchLoc"),
              u = n.call(i, "finallyLoc");
            if (c && u) {
              if (this.prev < i.catchLoc) return handle(i.catchLoc, !0);
              if (this.prev < i.finallyLoc) return handle(i.finallyLoc);
            } else if (c) {
              if (this.prev < i.catchLoc) return handle(i.catchLoc, !0);
            } else {
              if (!u) throw new Error("try statement without catch or finally");
              if (this.prev < i.finallyLoc) return handle(i.finallyLoc);
            }
          }
        }
      },
      abrupt: function abrupt(t, e) {
        for (var r = this.tryEntries.length - 1; r >= 0; --r) {
          var o = this.tryEntries[r];
          if (
            o.tryLoc <= this.prev &&
            n.call(o, "finallyLoc") &&
            this.prev < o.finallyLoc
          ) {
            var i = o;
            break;
          }
        }
        i &&
          ("break" === t || "continue" === t) &&
          i.tryLoc <= e &&
          e <= i.finallyLoc &&
          (i = null);
        var a = i ? i.completion : {};
        return (
          (a.type = t),
          (a.arg = e),
          i
            ? ((this.method = "next"), (this.next = i.finallyLoc), y)
            : this.complete(a)
        );
      },
      complete: function complete(t, e) {
        if ("throw" === t.type) throw t.arg;
        return (
          "break" === t.type || "continue" === t.type
            ? (this.next = t.arg)
            : "return" === t.type
            ? ((this.rval = this.arg = t.arg),
              (this.method = "return"),
              (this.next = "end"))
            : "normal" === t.type && e && (this.next = e),
          y
        );
      },
      finish: function finish(t) {
        for (var e = this.tryEntries.length - 1; e >= 0; --e) {
          var r = this.tryEntries[e];
          if (r.finallyLoc === t)
            return this.complete(r.completion, r.afterLoc), resetTryEntry(r), y;
        }
      },
      catch: function _catch(t) {
        for (var e = this.tryEntries.length - 1; e >= 0; --e) {
          var r = this.tryEntries[e];
          if (r.tryLoc === t) {
            var n = r.completion;
            if ("throw" === n.type) {
              var o = n.arg;
              resetTryEntry(r);
            }
            return o;
          }
        }
        throw new Error("illegal catch attempt");
      },
      delegateYield: function delegateYield(e, r, n) {
        return (
          (this.delegate = { iterator: values(e), resultName: r, nextLoc: n }),
          "next" === this.method && (this.arg = t),
          y
        );
      }
    }),
    e
  );
}
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    reject(error);
    return;
  }
  if (info.done) {
    resolve(value);
  } else {
    Promise.resolve(value).then(_next, _throw);
  }
}
function _asyncToGenerator(fn) {
  return function () {
    var self = this,
      args = arguments;
    return new Promise(function (resolve, reject) {
      var gen = fn.apply(self, args);
      function _next(value) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }
      function _throw(err) {
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }
      _next(undefined);
    });
  };
}
function foo() {
  return _foo.apply(this, arguments);
}
function _foo() {
  _foo = _asyncToGenerator(
    /*#__PURE__*/ _regeneratorRuntime().mark(function _callee() {
      return _regeneratorRuntime().wrap(function _callee$(_context) {
        while (1)
          switch ((_context.prev = _context.next)) {
            case 0:
              _context.next = 2;
              return Promise.resolve();
            case 2:
            case "end":
              return _context.stop();
          }
      }, _callee);
    })
  );
  return _foo.apply(this, arguments);
}

Для реализации такой простой, с виду, конструкции, Babel имплементирует механизм генератором с набором проверок и настроек, что приводит к итоговому листингу в 490 строк кода.

TypeScript

async function foo() {
  await Promise.resolve();
}
// == tsc ==
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
function foo() {
    return __awaiter(this, void 0, void 0, function () {
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, Promise.resolve()];
                case 1:
                    _a.sent();
                    return [2 /*return*/];
            }
        });
    });
}

TypeScript тоже использует, в этом случае, генераторы, но импелементация гораздо проще и итоговый листинг - всего 48 строк.

Spread-оператор

Spread-оператор появился в версии ES9. До этого, традиционно, по необходимости, применялся метод Object.assign, но с появлением spread и rest операторов код стал выглядеть примерно вот так

ECMAScript

const a = {...({})}
// == babel ==
function _typeof(o) {
  "@babel/helpers - typeof";
  return (
    (_typeof =
      "function" == typeof Symbol && "symbol" == typeof Symbol.iterator
        ? function (o) {
            return typeof o;
          }
        : function (o) {
            return o &&
              "function" == typeof Symbol &&
              o.constructor === Symbol &&
              o !== Symbol.prototype
              ? "symbol"
              : typeof o;
          }),
    _typeof(o)
  );
}
function ownKeys(e, r) {
  var t = Object.keys(e);
  if (Object.getOwnPropertySymbols) {
    var o = Object.getOwnPropertySymbols(e);
    r &&
      (o = o.filter(function (r) {
        return Object.getOwnPropertyDescriptor(e, r).enumerable;
      })),
      t.push.apply(t, o);
  }
  return t;
}
function _objectSpread(e) {
  for (var r = 1; r < arguments.length; r++) {
    var t = null != arguments[r] ? arguments[r] : {};
    r % 2
      ? ownKeys(Object(t), !0).forEach(function (r) {
          _defineProperty(e, r, t[r]);
        })
      : Object.getOwnPropertyDescriptors
      ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t))
      : ownKeys(Object(t)).forEach(function (r) {
          Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
        });
  }
  return e;
}
function _defineProperty(obj, key, value) {
  key = _toPropertyKey(key);
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }
  return obj;
}
function _toPropertyKey(t) {
  var i = _toPrimitive(t, "string");
  return "symbol" == _typeof(i) ? i : String(i);
}
function _toPrimitive(t, r) {
  if ("object" != _typeof(t) || !t) return t;
  var e = t[Symbol.toPrimitive];
  if (void 0 !== e) {
    var i = e.call(t, r || "default");
    if ("object" != _typeof(i)) return i;
    throw new TypeError("@@toPrimitive must return a primitive value.");
  }
  return ("string" === r ? String : Number)(t);
}
var a = _objectSpread({}, {});

В случае с Babel, здесь создается функция _objectSpread, которая обходит объект по его ключам и клонирует значения.

TypeScript

const a = {...({})}
// == tsc ==
var __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
var a = __assign({}, ({}));

TypeScript же, пытается использовать классический Object.assign, и только если его нет в данном окружении, имплементируется простой алгоритм обхода ключей объекта.

Заключение

В статье приведен даклеко не полный список всех возможностей и нововведений в специфакацию ECMAScript и документацию TypeScript. Здесь мы рассмотрели только те моменты, в которых могут расходиться, с точки зрения реализации.

В целом, резюмируя все выше сказанное, ES6+ (в частности, реализованный посредством Babel-парсера) генерирует относительно безопасный итоговый код, более стойкий ошибкам разработки и к злонамеренному вмешательству. Однако, цена тому - громоздкие конструкции и, как следствие, скорость генерации.

TypeScript же, в противовес, придерживается подхода к валидации на этапе compile time, т.е. код с ошибками, в теории, не должен быть собран, или, по крайней мере, об ошибках будет сразу сообщено. Это позволяет опустить всевозможные проверки на уровне runtime, что делает итоговый код гораздо меньше и легче. Однако, всегда остается, вполне осязаемая опасность недобросовестной разработки (type-check можно проигнорировать) и злонамеренного вмешательства.

Что считать лучшим подходом - вопрос сложный. Каждый разработчик и каждая команда решает его самостоятельно, исходя из своих собственных реалий. Часто, решение основывается на функциональных возможностях языка. Так, по мимо реализации возможностей стандарта ECMAScript, TypeScript имеет большие возможности в части типизации (собственно, это его прямо назначение), чего лишен ES6+. Этого критерия, часто, достаточно, чтобы сделать выбор в его пользу. С другой стороны, этот же плюс, не редко может оказаться минусом, так как требует определенных навыков и компетенций в команде разработки.

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


Мои телеграмм-каналы:

EN - https://t.me/frontend_almanac
RU - https://t.me/frontend_almanac_ru

English version: https://blog.frontend-almanac.com/X0JANevw3US