Package Exports
- ts-mixer
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (ts-mixer) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
ts-mixer
What is it?
ts-mixer is a lightweight package that brings mixins to TypeScript. Mixins in JavaScript are easy, but TypeScript introduces complications. ts-mixer deals with these complications for you and infers all of the intelligent typing you'd expect, including instance properties, methods, static properties, generics, and more.
Why another Mixin implementation?
It seems that no one has been able to implement TypeScript mixins gracefully. Mixins as described by the TypeScript docs are far less than ideal. Countless online threads feature half-working snippets, each one interesting but lacking in its own way.
My fruitless search has led me to believe that there is no perfect solution with the current state of TypeScript. Instead, I present a "tolerable" solution that attempts to take the best from the many different implementations while mitigating their flaws as much as possible.
Features
- can mix plain classes
- can mix classes that extend other classes
- can mix abstract classes (with caveats)
- can mix generic classes (with caveats)
- proper constructor argument typing (with caveats)
- proper handling of protected/private properties
- proper handling of static properties
- multiple options for mixing (ES6 proxies vs copying properties)
Caveats
- Mixing abstract classes requires a bit of a hack that may break in future versions of TypeScript. See dealing with abstract classes below.
- Mixing generic classes requires a more cumbersome notation, but it's still possible. See dealing with generics below.
- ES6 made it impossible to use
.apply(...)on class constructors, which means the only way to mix instance properties is to instantiate all the base classes, then copy the properties over to a new object. This means that (beyond initializing properties onthis), constructors cannot have side-effects involvingthis, or you will get unexpected results. Note that constructors need not be completey side-effect free; just when dealing withthis.
Non-features
instanceofsupport. Difficult to implement, and not hard to work around (if even needed at all).
Quick Start
Installation
$ npm install ts-mixeror if you prefer Yarn:
$ yarn add ts-mixerExamples
Minimal Example
import { Mixin } from 'ts-mixer';
class Foo {
protected makeFoo() {
return 'foo';
}
}
class Bar {
protected makeBar() {
return 'bar';
}
}
class FooBar extends Mixin(Foo, Bar) {
public makeFooBar() {
return this.makeFoo() + this.makeBar();
}
}
const fooBar = new FooBar();
console.log(fooBar.makeFooBar()); // "foobar"Mixing Abstract Classes
Abstract classes, by definition, cannot be constructed, which means they cannot take on the type, new(...args) => any, and by extension, are incompatible with ts-mixer. BUT, you can "trick" TypeScript into giving you all the benefits of an abstract class without making it technically abstract. The trick is just some strategic // @ts-ignore's:
import { Mixin } from 'ts-mixer';
// note that Foo is not marked as an abstract class
class Foo {
// @ts-ignore: "Abstract methods can only appear within an abstract class"
public abstract makeFoo(): string;
}
class Bar {
public makeBar() {
return 'bar';
}
}
class FooBar extends Mixin(Foo, Bar) {
// we still get all the benefits of abstract classes here, because TypeScript
// will still complain if this method isn't implemented
public makeFoo() {
return 'foo';
}
}Do note that while this does work quite well, it is a bit of a hack and I can't promise that it will continue to work in future TypeScript versions.
Mixing Generic Classes
Frustratingly, it is impossible for generic parameters to be referenced in base class expressions. No matter how you try to slice it, you will eventually run into Base class expressions cannot reference class type parameters.
The way to get around this is to leverage declaration merging, and a slightly different mixing function from ts-mixer: mix. It works exactly like Mixin, except it's a decorator, which means it doesn't affect the type information of the class being decorated. See it in action below:
import { mix } from 'ts-mixer';
class Foo<T> {
public fooMethod(input: T): T {
return input;
}
}
class Bar<T> {
public barMethod(input: T): T {
return input;
}
}
interface FooBar<T1, T2> extends Foo<T1>, Bar<T2> { }
@mix(Foo, Bar)
class FooBar<T1, T2> {
public fooBarMethod(input1: T1, input2: T2) {
return [this.fooMethod(input1), this.barMethod(input2)];
}
}Key takeaways from this example:
interface FooBar<T1, T2> extends Foo<T1>, Bar<T2> { }makes sureFooBarhas the typing we want, thanks to declaration merging@mix(Foo, Bar)wires things up "on the JavaScript side", since the interface declaration has nothing to do with runtime behavior.- The reason we have to use the
mixdecorator is that the typing produced byMixin(Foo, Bar)would conflict with the typing of the interface.mixhas no effect "on the TypeScript side," thus avoiding type conflicts.
Settings
ts-mixer has multiple strategies for mixing classes which can be configured by modifying Settings from ts-mixer. For example:
import { Settings, Mixin } from 'ts-mixer';
Settings.prototypeStrategy = 'proxy';
// then use `Mixin` as normal...Settings.prototypeStrategy
- Determines how ts-mixer will mix class prototypes together
- Possible values:
'copy'(default) - Copies all methods from the classes being mixed into a new prototype object. (This will include all methods up the prototype chains as well.) This is the default for ES5 compatibility, but it has the downside of stale references. For example, if you mixFooandBarto makeFooBar, then redefine a method onFoo,FooBarwill not have the latest methods fromFoo. If this is not a concern for you,'copy'is the best value for this setting.'proxy'- Uses an ES6 Proxy to "soft mix" prototypes. Unlike'copy', updates to the base classes will be reflected in the mixed class, which may be desirable. The downside is that method access is not as performant, nor is it ES5 compatible.
Settings.staticsStrategy
- Determines how static properties are inherited
- Possible values:
'copy'(default) - Simply copies all properties (minusprototype) from the base classes/constructor functions onto the mixed class. LikeSettings.prototypeStrategy = 'copy', this strategy also suffers from stale references, but shouldn't be a concern if you don't redefine static methods after mixing.'proxy'- Similar toSettings.prototypeStrategy, proxy's static method access to base classes. Has the same benefits/downsides.
Author
Tanner Nielsen tannerntannern@gmail.com
- Website - tannernielsen.com
- Github - tannerntannern