Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Add new __construct helper for better ES5/ES6 class interop #15397

Open
rbuckton opened this issue Apr 26, 2017 · 3 comments
Open

Proposal: Add new __construct helper for better ES5/ES6 class interop #15397

rbuckton opened this issue Apr 26, 2017 · 3 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@rbuckton
Copy link
Member

rbuckton commented Apr 26, 2017

I propose we add a new helper to assist with class instance construction runtime semantics when extending ES6 built-in classes while compiling with --target ES5.

Background

Our current emit for classes for --target ES5 assumes that the superclass follows the same runtime semantics as the classes we emit. Generally this means that the constructor can be called as a function via call() or apply(). However, a number of ES6 built-in classes are specified to throw when not used as a constructor (i.e. Promise, Map, etc.), and other ES6 built-in classes return a value when called, ignoring the this value provided to call() or apply() (i.e. Error, Array, etc.).

Previously we provided guidance for possible workarounds for this to support the latter scenario, but we do not currently have a solution for the former scenario.

Proposal

The following code listing describes a new __construct helper that we would need to emit for any file that contains an explicit (or implicit, for property declarations) super() call:

class MyPromise extends Promise {
  constructor(executor) {
    super(executor);
  }
}

// becomes...
var __extends = ...;
var __construct = (this && this.__construct) || (typeof Reflect !== "undefined" && Reflect.construct
    ? function (s, t, a, n) { return t !== null ? Reflect.construct(t, a, n) : s; }
    : function (s, t, a) { return t !== null && t.apply(s, a) || s; });

var MyPromise = (function (_super) {
  __extends(MyPromise, _super);
  function MyPromise(executor) {
    var _this = this;
    var _newTarget = this.constructor;
    _this = __construct(this, _super, [executor], _newTarget);
    return _this;
  }
  return MyPromise;
})(Promise);

Benefits

  • Allows down-level class emit to extend ES6 built-ins if running in an ES6 host by feature detecting Reflect.construct.
  • Falls back to the existing behavior when running in an ES5 host.
  • Handles extends null and extends x when x is null in the same way as existing behavior.
  • Handles custom return values from super in the same way as existing behavior.

Drawbacks

  • Larger helper footprint
  • Subclassing a built-in in an ES5 host has different runtime semantics than subclassing a built-in in an ES6 host:
    • In ES5, subclassing Array or Error will not have the correct prototype chain. The only solution is to explicitly set the prototype chain using the non-standard __proto__ property as per established guidance.
@rbuckton
Copy link
Member Author

Though it would add even more helper overhead, we could consider a best effort approach to address the subclassing drawback via __proto__:

var __construct = (this && this.__construct) || (typeof Reflect !== "undefined" && Reflect.construct
    ? function (s, t, a) { return t !== null && Reflect.construct(t, a, s.constructor); }
    : { __proto__: [] } instanceof Array
        ? function (s, t, a) { var p = s.__proto__, r = t !== null && t.apply(s, a) || s; return r.__proto__ = p, r; }
        : function (s, t, a) { return t !== null && t.apply(s, a) || s; });;

@csvn
Copy link

csvn commented Feb 22, 2018

Trying to use native WebComponents with Typescript is not very ergonomic right now, since first we need to target es2015 (or any higher target) and then use babel, which supports this via babel/babel#7020.

I know that adding tons of specific settings might want to be avoided, but would it not be a minimal change to have a flag (e.g. useClassReflectConstruct), which always outputs Reflect.construct(...) instead of .apply(...)?

It would be simple to include a polyfill for Reflect.construct in es5 browsers and set this option in tsconfig.json to support using Web components and extending native objects without hassle.

There are three other issues open for the same feature. What is blocking progress on this? Is it something that could be added as a PR?

@Jessidhia
Copy link

Jessidhia commented Feb 23, 2018

The problem with compiling to use Reflect.construct is that the this.constructor property must not be overwritten or otherwise obstructed in any way; but this is the same caveat as the one needed to support compiling new.target

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants