JSPM

  • Created
  • Published
  • Downloads 1163919
  • Score
    100M100P100Q189408F
  • 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

Build Status Coverage Status dependencies Status

Why another Mixin library?

It seems that no one has been able to provide an acceptable way to gracefully implement the mixin pattern with TypeScript. Mixins as described by the TypeScript docs are far less than ideal. Countless online threads feature half-working snippets. Some are elegant, but fail to work properly with static properties. Others solve static properties, but they don't work well with generics. Some are memory-optimized, but force you to write the mixins in an awkward, cumbersome format.

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

  • Support for mixing plain TypeScript classes¹
  • Support for mixing classes that extend other classes
  • Support for protected and private properties
  • Support for classes with generics (woot!)²
  • Automatic inference of the mixed class type²
  • Proper handling of static properties³

Caveats

  1. Only targeting ES5 is currently supported Targeting ES6 ("target": "es6" in your tsconfig.json) will likely cause issues since ES6 doesn't allow you to call constructor functions without the new keyword, which is crucial for mixins to work at all. If you must use ES6, you must define your classes "the old way" rather than with the new class keyword to avoid runtime errors.
  2. Some mixin implementations require you to do something like Mixin<A & B>(A, B) in order for the types to work correctly. ts-mixer is able to infer these types, so you can just do Mixin(A, B), except when generics are involved. See Dealing with Generics.
  3. Due to the way constructor types work in TypeScript, it's impossible to specify a type that is both a constructor and has specific properties. Static properties are still accessible "on the JavaScript side," but you have to make some type assertions to convince TypeScript that you can access them. See Dealing with Static Properties.

Non-features

  • instanceof support; Because this library is intended for use with TypeScript, running an instanceof check is generally not needed. Additionally, adding support can have negative effects on performance. See the MDN documentation for more information.

Getting Started

Installation

npm i --save ts-mixer

Documentation

If you're looking for more complete documentation, go here. If you just need a few tips to get started, keep reading.

Examples

Basic Example

import {Mixin} from 'ts-mixer';

class Person {
    protected name: string;

    constructor(name: string) {
        this.name = name;
    }
}

class RunnerMixin {
    protected runSpeed: number = 10;

    public run(){
        console.log('They are running at', this.runSpeed, 'ft/sec');
    }
}

class JumperMixin {
    protected jumpHeight: number = 3;

    public jump(){
        console.log('They are jumping', this.jumpHeight, 'ft in the air');
    }
}

class LongJumper extends Mixin(Person, RunnerMixin, JumperMixin) {
    public longJump() {
        console.log(this.name, 'is stepping up to the event.');
        
        this.run();
        this.jump();
        
        console.log('They landed', this.runSpeed * this.jumpHeight, 'ft from the start!');
    }
}

Dealing with Static Properties

Consider the following scenario:

import {Mixin} from 'ts-mixer';

class Person {
    public static TOTAL: number = 0;
    constructor() {
        (<typeof Person>this.constructor).TOTAL ++;
    }
}

class StudentMixin {
    public study() { console.log('I am studying so hard') }
}

class CollegeStudent extends Mixin(Person, StudentMixin) {}

It would be expected that class CollegeStudent should have the property TOTAL since CollegeStudent inherits from Person. The Mixin function properly sets up the inheritance of this static property, so that modifying it on the CollegeStudent class will also affect the Person class:

let p1 = new Person();
let cs1 = new CollegeStudent();

Person.TOTAL === 2; 		// true
CollegeStudent.TOTAL === 2;	// true

The only issue is that due to the impossibility of specifying properties on a constructor type, you must use some type assertions to keep the TypeScript compiler from complaining:

CollegeStudent.TOTAL ++;                           // error
(<any>CollegeStudent).TOTAL ++;                    // ok
(<typeof Person><unknown>CollegeStudent).TOTAL++;  // ugly, but better

Dealing with Generics

Normally, the Mixin function is able to figure out the class types and produce an appropriately typed result. However, when generics are involved, the Mixin function is not able to correctly infer the type parameters. Consider the following:

import {Mixin} from 'ts-mixer';

class GenClassA<T> {
    methodA(input: T) {}
}
class GenClassB<T> {
    methodB(input: T) {}
}

Now let's say that we want to mix these two generic classes together, like so:

class Mixed extends Mixin(GenClassA, GenClassB) {}

But we run into trouble here because we can't pass our type parameters along with the arguments to the Mixin function. How can we resolve this?

Option 1: Passing Type Parameters

One solution is to pass your type information as type parameters to the Mixin function (note that the string and number types are arbitrary):

class Mixed extends Mixin<GenClassA<string>, GenClassB<number>>(GenClassA, GenClassB) {}

This really works quite well. However, it gets worse if you need the mixins to reference type parameters on the class, because this won't work:

class Mixed<A, B> extends Mixin<GenClassA<A>, GenClassB<B>>(GenClassA, GenClassB) {}
// Error: TS2562: Base class expressions cannot reference class type parameters.

Option 2: Using Class Decorators and Interface Merging

To solve this issue, we can make simultaneous use of class decorators and interface merging to create the proper class typing. It has the benefit of working without wrapping the class in a function, but because it depends on class decorators, the solution may not last for future versions of TypeScript. (I tested on 3.1.3)

Either way, it's a super cool solution. Consider the following:

import {MixinDecorator} from 'ts-mixer';

@MixinDecorator(GenClassA, GenClassB)
class Mixed<A, B> {
    someAdditonalMethod(input1: A, input2: B) {}
}

The first thing to note is the MixinDecorator import. This function is very similar to the Mixin function, but in a decorator format. Decorators have the annoying property that even though they may modify the shape of the class they decorate "on the JavaScript side," the types don't update "on the TypeScript side." So as far as the TypeScript compiler is concerned in the example above, class Mixed just has that one method, even though the decorator is really adding methods from the mixed generic classes.

How do we convince TypeScript that Mixed has the additional methods? An attempt at a solution might look like this:

@MixinDecorator(GenClassA, GenClassB)
class Mixed<A, B> implements GenClassA<A>, GenClassB<B> {
    someAdditonalMethod(input1: A, input2: B) {}
}

But now TypeScript will complain that Mixed doesn't implement GenClassA and GenClassB correctly, because it can't see the changes made by the decorator. Instead, we can use interface merging:

@MixinDecorator(GenClassA, GenClassB)
class Mixed<A, B> {
    someAdditonalMethod(input1: A, input2: B) {}
}
interface Mixed<A, B> extends GenClassA<A>, GenClassB<B> {}

TADA! We now have a truly generic class that uses generic mixins!

It's worth noting however that it's only through the combination of TypeScript's failure to consider type modifications with decorators in conjunction with interface merging that this works. If we attempted interface merging without the decorator, we would run into trouble:

interface Mixed<A, B> extends GenClassA<A>, GenClassB<B> {}
class Mixed<A, B> extends Mixin(GenClassA, GenClassB) {
    newMethod(a: A, b: B) {}
}

// Error:TS2320: Interface 'Mixed<A, B>' cannot simultaneously extend types 'GenClassA<{}> & GenClassB<{}>' and 'GenClassA<A>'.
// Named property 'methodA' of types 'GenClassA<{}> & GenClassB<{}>' and 'GenClassA<A>' are not identical.

We get this error because when the Mixin function is used in an extends clause, TypeScript is smart enough extract type information, which conflicts with the interface definition above it; when Mixin is given the generic classes as arguments, it doesn't receive their type parameters and they default to {}. Even if you try to // @ts-ignore ignore it, the type checker will prefer the types of the Mixin function over those of the interface.

Contributing

All contributions are welcome! To get started, simply fork and clone the repo, run npm install, and get to work. Once you have something you'd like to contribute, be sure to run npm run lint && npm run test locally, then submit a PR.

Tests are very important to consider and I will not accept any PRs that are poorly tested. Keep the following in mind:

  • If you add a new feature, please make sure it's covered by a test case. Typically this should get a dedicated *.test.ts file in the test directory, so that all of the nuances of the feature can be adequately covered.
  • If you are contributing a bug fix, you must also write at least one test to verify that the bug is fixed. If the bug is directly related to an existing feature, try to include the test in the relevant existing file. If the bug is highly specific, it may make sense to give the test its own file; use discretion.

Author

Tanner Nielsen tannerntannern@gmail.com