JSPM

  • Created
  • Published
  • Downloads 1163919
  • Score
    100M100P100Q189291F
  • License MIT

A very small TypeScript library that provides tolerable Mixin functionality.

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

npm version Build Status Coverage Status Minified Size Conventional Commits

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.

Quick start guide

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 on this), constructors cannot have side-effects involving this, or you will get unexpected results. Note that constructors need not be completey side-effect free; just when dealing with this.

Non-features

  • instanceof support. Difficult to implement, and not hard to work around (if even needed at all).

Quick Start

Installation

$ npm install ts-mixer

or if you prefer Yarn:

$ yarn add ts-mixer

Examples

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"

Play with this example

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';
    }
}

Play with this example

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)];
    }
}

Play with this example

Key takeaways from this example:

  • interface FooBar<T1, T2> extends Foo<T1>, Bar<T2> { } makes sure FooBar has 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 mix decorator is that the typing produced by Mixin(Foo, Bar) would conflict with the typing of the interface. mix has 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 mix Foo and Bar to make FooBar, then redefine a method on Foo, FooBar will not have the latest methods from Foo. 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 (minus prototype) from the base classes/constructor functions onto the mixed class. Like Settings.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 to Settings.prototypeStrategy, proxy's static method access to base classes. Has the same benefits/downsides.

Author

Tanner Nielsen tannerntannern@gmail.com