What is ts-mixer?
The ts-mixer package is a TypeScript library that provides utilities for creating mixins and multiple inheritance in TypeScript. It allows developers to combine multiple classes into one, enabling more flexible and reusable code structures.
What are ts-mixer's main functionalities?
Basic Mixin
This feature allows you to create a new class that combines the methods and properties of multiple classes. In this example, class C inherits methods from both class A and class B.
const { Mixin } = require('ts-mixer');
class A {
methodA() {
console.log('Method A');
}
}
class B {
methodB() {
console.log('Method B');
}
}
class C extends Mixin(A, B) {}
const c = new C();
c.methodA(); // Method A
c.methodB(); // Method B
Advanced Mixin with Constructor
This feature demonstrates how to handle constructors when mixing classes. The new class C can initialize properties from both class A and class B.
const { Mixin } = require('ts-mixer');
class A {
constructor(public name: string) {}
methodA() {
console.log(`Method A: ${this.name}`);
}
}
class B {
constructor(public age: number) {}
methodB() {
console.log(`Method B: ${this.age}`);
}
}
class C extends Mixin(A, B) {
constructor(name: string, age: number) {
super(name, age);
}
}
const c = new C('John', 30);
c.methodA(); // Method A: John
c.methodB(); // Method B: 30
Mixin with Interfaces
This feature shows how to use mixins with interfaces. Class C implements both IA and IB interfaces and inherits methods from class A and class B.
const { Mixin } = require('ts-mixer');
interface IA {
methodA(): void;
}
interface IB {
methodB(): void;
}
class A implements IA {
methodA() {
console.log('Method A');
}
}
class B implements IB {
methodB() {
console.log('Method B');
}
}
class C extends Mixin(A, B) implements IA, IB {}
const c = new C();
c.methodA(); // Method A
c.methodB(); // Method B
Other packages similar to ts-mixer
mixwith
The mixwith package provides a similar functionality for creating mixins in JavaScript and TypeScript. It offers a more flexible and modern approach to mixins, but ts-mixer is more TypeScript-centric and provides better type safety.
mixin-deep
The mixin-deep package allows deep merging of objects and mixins. While it is more focused on object merging rather than class-based mixins, it can be used to achieve similar results in a different way.
lodash
Lodash is a utility library that provides a wide range of functions, including mixin capabilities. However, it is more general-purpose and not specifically designed for TypeScript or class-based mixins.
ts-mixer
![Conventional Commits](https://badgen.net/badge/conventional%20commits/1.0.0/yellow)
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)
- supports class, method, and property decorators (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.
- Using decorators in mixed classes also requires a more cumbersome notation. See dealing with decorators 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());
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';
class Foo {
public abstract makeFoo(): string;
}
class Bar {
public makeBar() {
return 'bar';
}
}
class FooBar extends Mixin(Foo, Bar) {
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.
Mixing with Decorators
Popular libraries such as class-validator and TypeORM use decorators to add functionality. Unfortunately, ts-mixer
has no way of knowing what these libraries do with the decorators behind the scenes. So if you want these decorators to be "inherited" with classes you plan to mix, you first have to wrap them with a special decorate
function export by ts-mixer
. Here's an example using class-validator
:
import { IsBoolean, IsIn } from 'class-validator';
import { Mixin, decorate } from 'ts-mixer';
class Disposable {
@decorate(IsBoolean())
isDisposed: boolean = false;
}
class Statusable {
@decorate(IsIn(['red', 'green']))
status: string = 'green';
}
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';
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