General solution
The following code produces the same effect in TypeScript (staticField
is on the prototype chain and not on the derived object). Note, however, that using a truly static
field in the base class is easier: you won't need to write as BaseClass
in NewBaseClass
.
- TypeScript 3.8.3 doesn't fully accept it: it complains about
DerivedClass
, saying "A mixin class must have a constructor with a single rest parameter of type 'any[]'". However this error can be be suppressed with // @ts-ignore
.
TypeScript 3.6.5 doesn't seem to understand that baseClass
is non-empty and therefore gives multiple errors. It also says "Return type of exported function has or is using private name 'DerivedClass'" which is weird since NewDerivedClass
is not exported. A workaround for the latter error is to define a matching interface and use it as the return type:
interface DerivedClass_ {
new (iF: number, dF: number): {
derivedField: number;
dump(): void;
}
}
interface BaseClass {
new (iF: number): {
instanceField: number;
staticField: string;
};
}
function NewBaseClass(sF: string): BaseClass {
class DynamicBaseClass {
instanceField: number;
staticField?: string; // a value assigned here wouldn't be on the prototype
constructor(iF: number) { this.instanceField = iF; }
}
DynamicBaseClass.prototype.staticField = sF;
return DynamicBaseClass as BaseClass;
}
function NewDerivedClass<Base extends BaseClass>(baseClass: Base) {
// @ts-ignore "A mixin class must have a constructor with a single rest parameter..."
class DerivedClass extends baseClass {
derivedField: number;
constructor(iF: number, dF: number) {
super(iF);
this.derivedField = dF;
}
dump() {
console.log("instanceField=" + this.instanceField +
" derivedField=" + this.derivedField +
" staticField=" + this.staticField +
" base=" + (this as any).__proto__.__proto__.constructor.name);
}
}
return DerivedClass;
}
var BaseClass1 = NewBaseClass("dynamic prototype chain #1");
var BaseClass2 = NewBaseClass("dynamic prototype chain #2");
new (NewDerivedClass(BaseClass1))(3, 33).dump();
new (NewDerivedClass(BaseClass1))(4, 44).dump();
new (NewDerivedClass(BaseClass2))(5, 55).dump();
new (NewDerivedClass(BaseClass2))(6, 66).dump();
The compiler's output looks like this in TS 3.6.3 and works as expected:
"use strict";
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 __());
};
})();
function NewBaseClass(sF) {
var DynamicBaseClass = /** @class */ (function () {
function DynamicBaseClass(iF) {
this.instanceField = iF;
}
return DynamicBaseClass;
}());
DynamicBaseClass.prototype.staticField = sF;
return DynamicBaseClass;
}
function NewDerivedClass(baseClass) {
// @ts-ignore "A mixin class must have a constructor with a single rest parameter of type 'any[]'."
var DerivedClass = /** @class */ (function (_super) {
__extends(DerivedClass, _super);
function DerivedClass(iF, dF) {
var _this = _super.call(this, iF) || this;
_this.derivedField = dF;
return _this;
}
DerivedClass.prototype.dump = function () {
console.log("instanceField=" + this.instanceField +
" derivedField=" + this.derivedField +
" staticField=" + this.staticField +
" base=" + this.__proto__.__proto__.constructor.name);
};
return DerivedClass;
}(baseClass));
return DerivedClass;
}
var BaseClass1 = NewBaseClass("dynamic prototype chain #1");
var BaseClass2 = NewBaseClass("dynamic prototype chain #2");
new (NewDerivedClass(BaseClass1))(3, 33).dump();
new (NewDerivedClass(BaseClass1))(4, 44).dump();
new (NewDerivedClass(BaseClass2))(5, 55).dump();
new (NewDerivedClass(BaseClass2))(6, 66).dump();
I see it uses Object.setPrototypeOf
which MDN warns us not to use for performance reasons. I hope the TypeScript people know what they are doing!
Technique for "cheap" data sharing
If the goal is simply to share data among many instances without consuming any memory on individual instances, it can be done much more simply like this:
interface DynamicClass_ { // not needed in TypeScript 3.8
new (iF: number, dF: number): {
instanceField: number;
derivedField: number;
};
}
function NewClass(staticField: string, foo: any): DynamicClass_ {
class DynamicClass {
constructor(public instanceField: number,
public derivedField: number) { }
dump() {
console.log("instanceField=" + this.instanceField +
" derivedField=" + this.derivedField +
" staticField=" + staticField + // <<<<<<<<<<<<<<<<<<<<<<<<<
" foo=" + foo); // <<<<<<<<<<<<<<<<<<<<<<<<<
}
}
return DynamicClass;
}
Notice that dump()
can reference parameters without storing them in the class anywhere! In general, the JS runtime must create some kind of heap object for class functions like dump()
to share. Logically, it cannot store the parameters (staticField
etc.) in the instance (this
), because it is possible to change this
using code like
new (NewClass(...))(...).dump.bind(otherThis)
- and yet the rebound
dump
will still have access to parameters of
NewClass
.
I'm pretty sure that the objects representing the functions inside DynamicClass
must be created anew every time NewClass
is called, because these objects are accessible to JS programs. So any technique that involves returning classes or functions from another function will incur a certain memory cost. Depending on the circumstances, this cost may be smaller or larger than storing data in the class instances.
When using this technique, it can be useful to copy parameters into the prototype for debugging purposes:
function NewClass(staticField: string): DynamicClass_ {
class DynamicClass {
...
}
let proto: any = DynamicClass.prototype;
proto.staticField = staticField;
return DynamicClass;
}