michaelolof / typescript-mix

A tweaked implementation of TypeScript's default applyMixins(...) idea using ES7 decorators
https://www.npmjs.com/package/typescript-mix
87 stars 8 forks source link
mixins traits-decorator typescript typescript-mixins

TypeScript Mix

A tweaked implementation of TypeScript's default applyMixins(...) idea using ES7 decorators.

Breaking Changes from Version 3.0.0 upwards

See Breaking Changes Explained

Dependencies

Installation

npm install --save typescript-mix

Features

Goals

Why I wrote yet another Mixin Library.

The mixin pattern is somewhat a popular pattern amongst JavaScript/TypeScript devs as it gives the power of "mixin in" additional functionality to a class. The official way of using mixins as declared by Microsoft in TypeScript can be really verbose to downright unreadable.

How to use

The 'use' decorator

Program to an interface.

interface Buyer {
  price: number
  buy(): void
  negotiate(): void
}

Create a reusable implementation for that interface and that interface alone (Mixin)

const Buyer: Buyer = {
  price: undefined,
  buy() {
    console.log("buying items at #", this.price );
  },
  negotitate(price: number) {
    console.log("currently negotiating...");
    this.price = price;
  },
}

Define another mixin this time using a Class declaration.

class Transportable {
  distance:number;
  transport() {
    console.log(`moved ${this.distance}km.`);
  }
}

Define a concrete class that utilizes the defined mixins.

import use from "typescript-mix";

class Shopperholic {
  @use( Buyer, Transportable ) this

  price = 2000;
  distance = 140;
}

const shopper = new Shopperholic();
shopper.buy() // buying items at #2000
shopper.negotiate(500) // currently negotiating...
shopper.price // 500
shopper.transport() // moved 140km

What about intellisense support?

We trick typescript by using the inbuilt interface inheritance and declaration merging ability.

interface Shopperholic extends Buyer, Transportable {}

class Shopperholic {
  @use( Buyer, Transportable ) this

  price = 2000;
  distance = 140;
}

The 'delegate' decorator

The delegate decorator is useful when we want specific functionality mixed into the client.

class OtherClass {
  simpleMethod() {
    console.log("This method has no dependencies");
  }
}

function workItOut() {
  console.log("I am working it out.")
}

class MyClass {
  @delegate( OtherClass.prototype.simpleMethod )
  simpleMethod:() => void

  @delegate( workItOut ) workItOut:() => void
}

const cls = new MyClass();
cls.simpleMethod() // This method has no dependencies
cls.workItOut() // I am working it out

Things to note about this library?

Advantages

Breaking Changes Explained

The delegate decorator

The addition of the delegate decorator now means module is imported as:

import { use, delegate } from "typescript-mix"

Multiple Mixins with the same method.

Consider the following piece of code. alt text

Cleint One uses two mixins that contain the same method mixIt(). How do we resolve this? Which method gets picked?. One advantage of extending interfaces as we've defined above is that we're essentially telling TypeScript to mix-in the two mixin interfaces into the ClientOne interface. So how does TypeScript resolve this?

alt text

Notice that TypeScript's intellisense calls MixinOne.mixIt() method. Therefore to be consistent with TypeScript and avoid confusion the '@use' decorator also implements MixinOne.mixIt() method.