Dynamic multiple inheritance for JavaScript and TypeScript. Without mixins.
Polytype is a library that adds support for dynamic multiple inheritance to JavaScript and TypeScript with a simple syntax. “Dynamic” means that changes to base classes at runtime are reflected immediately in all derived classes just like programmers would expect when working with single prototype inheritance.
As of today, Polytype runs in current versions of all major browsers and in Node.js(*).
- Python style multiple inheritance
- Works in Node.js and in most browsers
- Full TypeScript support
- Zero dependencies
- Qualified or unqualified access to all base class features
- constructors
- methods, getters and setters – both static and nonstatic
- value properties on base classes and base instance prototypes
- public class fields (in engines that support them)
in
,instanceof
andisPrototypeOf
integration
Polytytpe is available in two flavors: a module build (comprising CommonJS and ECMAScript modules) with exported definitions and a script build where all definitions are accessible through global objects. Apart from this, both builds provide the same features and are available in the standard package.
If you are using Node.js, you can install Polytype with npm.
npm install polytype
Then you can import it in your code like any module.
const { classes } = require("polytype"); // CommonJS syntax
or
import { classes } from "polytype"; // ECMAScript module syntax
In TypeScript you can also import certain types where necessary.
import { SuperConstructorInvokeInfo } from "polytype";
Alternatively, you can import the script build at the start of your application and access Polytype definitions through global objects.
require("polytype/global"); // CommonJS syntax
or
import "polytype/global"; // ECMAScript module syntax
In an HTML‐based application, the script build of Polytype can be simply embedded. Just download polytype.min.js from GitHub and include it in your HTML file.
<script src="polytype.min.js"></script>
Alternatively, you can hotlink the latest release using a CDN of your choice.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/lib/polytype.min.js"></script>
If your browser application already uses ECMAScript modules, you can also import the module build
(“.mjs”) in contexts where Polytype specific definitions like classes
are required.
This has the advantage to avoid possible naming conflicts on global objects.
import { classes } from "https://cdn.jsdelivr.net/npm/[email protected]/lib/polytype.min.mjs";
For example, declare a derived class ColoredCircle
that inherits from both base classes Circle
and ColoredObject
.
class Circle
{
constructor(centerX, centerY, radius)
{
this.moveTo(centerX, centerY);
this.radius = radius;
}
get diameter() { return this.radius * 2; }
set diameter(diameter) { this.radius = diameter / 2; }
moveTo(centerX, centerY)
{
this.centerX = centerX;
this.centerY = centerY;
}
toString()
{
return `circle with center (${this.centerX}, ${this.centerY}) and radius ${this.radius}`;
}
}
class ColoredObject
{
constructor(color) { this.color = color; }
static areSameColor(obj1, obj2) { return obj1.color === obj2.color; }
paint() { console.log(`painting in ${this.color}`); }
toString() { return `${this.color} object`; }
}
class ColoredCircle
extends classes(Circle, ColoredObject) // Base classes as comma‐separated params
{
// Add methods here.
}
const c = new ColoredCircle();
c.moveTo(42, 31);
c.radius = 1;
c.color = "red";
console.log(c.centerX, c.centerY); // 42, 31
console.log(c.diameter); // 2
c.paint(); // "painting in red"
As usual, the keyword super
invokes a base class method or property accessor when used inside a
derived class.
class ColoredCircle
extends classes(Circle, ColoredObject)
{
paint()
{
super.paint(); // Using method paint from some base class
}
}
If different base classes include a member with the same name, the syntax
super.class(DirectBaseClass).member
can be used to make the member access unambiguous.
class ColoredCircle
extends classes(Circle, ColoredObject)
{
toString()
{
// Using method toString from base class Circle
const circleString = super.class(Circle).toString();
return `${circleString} in ${this.color}`;
}
}
More generally, super.class(DirectBaseClass)[propertyKey]
can be used to reference a property of a
particular base class in the body of a derived class.
Note: In TypeScript, the syntax described here cannot be used to access protected instance members, so it is currently not possible to disambiguate between protected instance members having the same name, the same index or the same symbol in different base classes.
Static methods and property accessors are inherited, too.
ColoredCircle.areSameColor(c1, c2)
same as
ColoredObject.areSameColor(c1, c2)
Use arrays to group together parameters for each base constructor in the derived class constructor.
class ColoredCircle
extends classes(Circle, ColoredObject)
{
constructor(centerX, centerY, radius, color)
{
super
(
[centerX, centerY, radius], // Circle constructor params
[color] // ColoredObject constructor params
);
}
}
If you prefer to keep parameter lists associated to their base classes explicitly without relying on order, there is an alternative syntax.
class GreenCircle
extends classes(Circle, ColoredObject)
{
constructor(centerX, centerY, radius)
{
super
(
{ super: ColoredObject, arguments: ["green"] },
{ super: Circle, arguments: [centerX, centerY, radius] }
);
}
}
There is no need to specify an array of parameters for each base constructor. If the parameter arrays are omitted, the base constructors will still be invoked without parameters.
class WhiteUnitCircle
extends classes(Circle, ColoredObject)
{
constructor()
{
super(); // Base constructors invoked without parameters
this.centerX = 0;
this.centerY = 0;
this.radius = 1;
this.color = "white";
}
}
The instanceof
operator works just like it should.
const c = new ColoredCircle();
console.log(c instanceof Circle); // true
console.log(c instanceof ColoredObject); // true
console.log(c instanceof ColoredCircle); // true
console.log(c instanceof Object); // true
console.log(c instanceof Array); // false
In pure JavaScript, the expression
B.prototype instanceof A
determines if A
is a base class of class B
.
Polytype takes care that this test still works well with multiple inheritance.
console.log(ColoredCircle.prototype instanceof Circle); // true
console.log(ColoredCircle.prototype instanceof ColoredObject); // true
console.log(ColoredCircle.prototype instanceof ColoredCircle); // false
console.log(ColoredCircle.prototype instanceof Object); // true
console.log(Circle.prototype instanceof ColoredObject); // false
The in
operator determines whether a property is in an object or in its prototype chain.
In the case of multiple inheritance, the prototype “chain” looks more like a directed graph, yet the
function of the in
operator is the same.
const c = new ColoredCircle();
console.log("moveTo" in c); // true
console.log("paint" in c); // true
console.log("areSameColor" in ColoredCircle); // true
console.log("areSameColor" in Circle); // false
console.log("areSameColor" in ColoredObject); // true
isPrototypeOf
works fine, too.
const c = new ColoredCircle();
console.log(Circle.prototype.isPrototypeOf(c)); // true
console.log(ColoredObject.prototype.isPrototypeOf(c)); // true
console.log(ColoredCircle.prototype.isPrototypeOf(c)); // true
console.log(Object.prototype.isPrototypeOf(c)); // true
console.log(Array.prototype.isPrototypeOf(c)); // false
console.log(Circle.isPrototypeOf(ColoredCircle)); // true
console.log(ColoredObject.isPrototypeOf(ColoredCircle)); // true
console.log(ColoredCircle.isPrototypeOf(ColoredCircle)); // false
console.log(Object.isPrototypeOf(ColoredCircle)); // false
console.log(Function.prototype.isPrototypeOf(ColoredCircle)); // true
In single inheritance JavaScript, the direct base class of a derived class is obtained with
Object.getPrototypeOf
.
const DirectBaseClass = Object.getPrototypeOf(DerivedClass);
If a class has no explicit extends
clause, Object.getPrototypeOf
returns Function.prototype
,
the base of all classes.
Of course this method cannot work with multiple inheritance, since there is no way to return
multiple classes without packing them in some kind of structure.
For this and other use cases, Polytype exports the function getPrototypeListOf
, which can be used
to get an array of direct base classes given a derived class.
const { getPrototypeListOf } = require("polytype"); // Or some other kind of import.
function getBaseNames(derivedClass)
{
return getPrototypeListOf(derivedClass).map(({ name }) => name);
}
console.log(getBaseNames(ColoredCircle)); // ["Circle", "ColoredObject"]
console.log(getBaseNames(Int8Array)); // ["TypedArray"]
console.log(getBaseNames(Circle)); // [""] i.e. [Function.prototype.name]
If you use the script build of Polytype, no functions will be exported.
Instead, getPrototypeListOf
will be defined globally as Object.getPrototypeListOf
.
If a property in a base class is added, removed or modified at runtime, the changes are immediately reflected in all derived classes.
const c = new ColoredCircle();
Circle.prototype.sayHello = () => console.log("Hello!");
c.sayHello(); // "Hello!"
Polytype has built‐in TypeScript support: you can take advantage of type checking while working with
multiple inheritance without installing any additional packages.
If you are using an IDE that supports TypeScript code completion like Visual Studio Code, you will
get multiple inheritance sensitive suggestions as you type.
A TypeScript version of the ColoredCircle
sample code above can be found in
ColoredCircle.ts
in the example folder.
Neither JavaScript nor TypeScript offer native support for multiple inheritance of any kind. Polytype strives to make up for this deficiency, but some important limitations remain.
In plain TypeScript, without Polytype, a for...in
iteration over a class constructor will
enumerate not only names of static fields defined on that class, but also names of static fields
defined on all base classes in its prototype chain.
class FooClass
{
static foo = "foo";
}
class BarClass extends FooClass
{
static bar = "bar";
}
for (const name in BarClass)
console.log(name); // Prints "bar" and "foo".
Additionally, some newer JavaScript engines have implemented support for
static public fields,
which work much like in TypeScript, but without transpiling.
Even in JavaScript, a for...in
iteration over a class constructor will include names of static
fields defined on base classes.
Anyway, this behavior breaks with Polytype inheritance on static fields: names of static fields
defined on base classes are not enumerated by for...in
statements if the inheritance line crosses
the constructor of a class in some extends classes(...)
clause.
class BazClass extends classes(FooClass)
{
static baz = "baz";
}
for (const name in BazClass)
console.log(name); // Prints just "baz".
For this reason, and because generally better alternatives exist, iterating
over Polytype classes and their derived classes with for...in
is not recommended.
Multiple base classes may expose members with the same name, the same index or the same symbol. When this happens, any unqualified access to one of those members will have to determine the implementation to be used. The approach taken by Polytype is to pick the implementation found in the first direct base class that contains the (possibly inherited) member.
class A
{ }
class B
{
whoAmI() { console.log("B"); }
}
class C
{
whoAmI() { console.log("C"); }
}
class ABC extends classes(A, B, C)
{ }
const abc = new ABC();
abc.whoAmI(); // Prints "B".
This is similar to the depth‐first search algorithm of old‐style classes in Python 2, but it is different from the behavior of several other programming languages that support multiple inheritance, and it may not match your expectations if you come from a C++ background.
TypeScript class members can have
protected
access: protected members are inherited just like public members but can only be accessed in the
body of the defining class itself or of a derived class.
If a derived class inherits from multiple base classes, it is possible for inherited members in
different base classes to share the same property key, i.e. the same name, the same index or the
same symbol.
In this case, Polytype provides the syntax super.class(DirectBaseClass)[propertyKey]
to specify
the base class containing the member to be accessed.
This works fine for all public or static members, but results in a compiler error when applied to
protected instance members.
class Apple
{
protected id: number;
}
class Banana
{
protected id: number;
}
class Bananapple extends classes(Apple, Banana)
{
get bananaId()
{
return super.class(Banana).id; // error TS2446: Property 'id' is protected…
}
}
As a workaround, you could use an intermediate class to expose the inherited member with a different name without making it public.
class BananaProxy extends Banana
{
get bananaId()
{
return super.id;
}
}
class Bananapple extends classes(Apple, Banana)
{ }
Polytype was successfully tested in the following browsers/JavaScript engines.
- Chrome 71+
- Safari 13+
- Edge 79+
- Firefox 67+
- Opera 58+
- Node.js 13.2+
The minimum supported TypeScript version is 3.5.
Bundlers and other tools that process uncompressed Polytype source files are required to parse ECMAScript 2020 or higher syntax.