TypeScript: Mixed es5/es2015 code with mixin classes causes runtime errors

TypeScript Version: 2.4.1 (but likely anything since 2.2)

When utilising code targeted at ES5 mixed with code targeted at ES6 causes a runtime error when using mixin classes. The real world use case was using a library that was targeted at ES5 for distribution compatibility reasons while downstream code is being targeted at ES2017 because of running in a known limited environment.

Code

Tagged.ts

// @target: es5
export interface Constructor<T> {
    new(...args: any[]): T;
    prototype: T;
}

export default function Tagged<T extends Constructor<{}>>(Base: T) {
    return class extends Base {
        _tag = '';
    }
}

TaggedExample.ts

// @target: es2015
import Tagged from './Tagged';

class Example {
  log() {
    console.log('Hello World!');
  }
}

const TaggedExample = Tagged(Example);

const taggedExample = new TaggedExample(); // Uncaught TypeError: Class constructor Example cannot be invoked without 'new'

Expected behavior:

No run-time errors.

Actual behavior:

A runtime error of Uncaught TypeError: Class constructor Example cannot be invoked without 'new'.

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 6
  • Comments: 15 (9 by maintainers)

Most upvoted comments

This is very problematic because it means my library (which rely on this) published in es5 cannot be consumed by JavaScript directly at all.

I can’t reproduce the error […]

This issue still exists, but as more and more projects move to targeting ES2015 and higher I’m not sure it is a high enough priority to be fixed as the chances of running afoul of this are smaller and smaller. I expected the largest cause of this comes from extending builtins like Map and Set, but extending builtins seems to be something TC39 members are actively discouraging these days (with the possible exception of Error).

@DanielRosenwasser do we want to close this, or take a fix? I put together a fix for this, but it requires adding the __construct helper I mentioned above, which would change the emit for everyone using subclassing and --target ES5. It also would require a change to tslib, and that would require anyone affected by the change to upgrade to the latest tslib.

Alternatively, we could fix this behind a flag for the rare cases where this happens.

Looks like this works with Babel, because it creates a function around the super class (_createSuper helper):

"use strict";

// the untranspiled chunk
class A {}

// transpiled class B extends A {} with babel

function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }

function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }

function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }

function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }

function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function () { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }

function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }

function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }

function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }

function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }

var B = /*#__PURE__*/function (_A) {
  _inherits(B, _A);

  var _super = _createSuper(B);

  function B() {
    _classCallCheck(this, B);

    return _super.apply(this, arguments);
  }

  return B;
}(A);

and new B() works.

TS uses A.apply(..) directly, which is invalid with native classes:

"use strict";

// the untranspiled chunk
class A {}

// transpiled class B extends A {} with TS

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 (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var B = /** @class */ (function (_super) {
    __extends(B, _super);
    function B() {
        return _super !== null && _super.apply(this, arguments) || this;
    }
    return B;
}(A));


new B() // fails!

/*
Uncaught TypeError: Class constructor A cannot be invoked without 'new'
    at new B (<anonymous>:18:42)
    at <anonymous>:1:1
B @ VM714:18
(anonymous) @ VM738:1
*/

Because this fails:

class C {}
C.apply({}, []) // Uncaught TypeError: Class constructor C cannot be invoked without 'new'

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

🎤 tap tap is this thing on? 😁

/cc @mhegazy