Skip to content

Commit

Permalink
Wrap up the mixin page
Browse files Browse the repository at this point in the history
  • Loading branch information
orta committed Jun 30, 2020
1 parent b8c7d13 commit 618ad90
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 42 deletions.
234 changes: 192 additions & 42 deletions packages/handbook-v1/en/Mixins.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ permalink: /docs/handbook/mixins.html
oneline: Using the mixin pattern with TypeScript
---

# Introduction

Along with traditional OO hierarchies, another popular way of building up classes from reusable components is to build them by combining simpler partial classes.
You may be familiar with the idea of mixins or traits for languages like Scala, and the pattern has also reached some popularity in the JavaScript community.

# How Does A Mixin Work?
## How Does A Mixin Work?

The pattern relies on using Generics with class inheritance to extend a base class.
To get started, we'll need a class which will have the mixin's applied:
TypeScript's best mixin support is done via the class expression pattern.
You can read more about how this pattern works in [JavaScript here](https://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/).

To get started, we'll need a class which will have the mixin's applied on top of:

```ts twoslash
class Sprite {
Expand All @@ -27,7 +28,7 @@ class Sprite {
}
```

Then
Then you need a type and a class which

This comment has been minimized.

Copy link
@htalvitie

htalvitie Jul 26, 2020

End of sentence missing.


```ts twoslash
// To get started, we need a type which we'll use to extend
Expand All @@ -40,8 +41,9 @@ type Constructor = new (...args: any[]) => {};
// for changing it with an encapsulated private property:

function Scale<TBase extends Constructor>(Base: TBase) {
return class extends Base {
return class Scaling extends Base {
// Mixins may not declare private/protected properties
// however, you can use ES2020 private fields
_scale = 1;

setScale(scale: number) {
Expand All @@ -55,61 +57,142 @@ function Scale<TBase extends Constructor>(Base: TBase) {
}
```

# Understanding the sample
With these all set up, then you can create a class which represents the base class with mixins applied:

The code sample starts with the two classes that will act as our mixins.
You can see each one is focused on a particular activity or capability.
We'll later mix these together to form a new class from both capabilities.
```ts twoslash
class Sprite {
name = "";
x = 0;
y = 0;

```ts
// Disposable Mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
constructor(name: string) {
this.name = name;
}
}
type Constructor = new (...args: any[]) => {};
function Scale<TBase extends Constructor>(Base: TBase) {
return class Scaling extends Base {
// Mixins may not declare private/protected properties
// however, you can use ES2020 private fields
_scale = 1;

setScale(scale: number) {
this._scale = scale;
}

get scale(): number {
return this._scale;
}
};
}
// ---cut---
// Compose a new class from the Sprite class,
// with the Mixin Scale applier:
const EightBitSprite = Scale(Sprite);

// Activatable Mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
const flappySprite = new EightBitSprite("Bird");
flappySprite.setScale(0.8);
console.log(flappySprite.scale);
```

## Constrained Mixins

In the above form, the mixin's have no underlying knowledge of the class which can make it hard to create the design you want.

To model this, we modify the original constructor type to accept a generic argument.

```ts twoslash
// This was our previous constructor:
type Constructor = new (...args: any[]) => {};
// Now we use a generic version which can apply a constraint on
// the class which this mixin is applied to
type GConstructor<T = {}> = new (...args: any[]) => T;
```

This allows for creating classes which only work with constrained base classes:

```ts twoslash
type GConstructor<T = {}> = new (...args: any[]) => T;
class Sprite {
name = "";
x = 0;
y = 0;

constructor(name: string) {
this.name = name;
}
}
// ---cut---
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<typeof Sprite>;
type Loggable = GConstructor<{ print: () => void }>;
```

Next, we'll create the class that will handle the combination of the two mixins.
Let's look at this in more detail to see how it does this:
Then you can create mixins which only work when you have a particular base to build on:

```ts
class SmartObject {
...
```ts twoslash
type GConstructor<T = {}> = new (...args: any[]) => T;
class Sprite {
name = "";
x = 0;
y = 0;

constructor(name: string) {
this.name = name;
}
}
type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
type Spritable = GConstructor<typeof Sprite>;
type Loggable = GConstructor<{ print: () => void }>;
// ---cut---

interface SmartObject extends Disposable, Activatable {}
function Jumpable<TBase extends Positionable>(Base: TBase) {
return class Jumpable extends Base {
jump() {
// This mixin will only work if it is passed a base
// class which has setPos defined because of the
// Positionable constraint.
this.setPos(0, 20);
}
};
}
```

The first thing you may notice in the above is that instead of trying to extend `Disposable` and `Activatable` in `SmartObject` class, we extend them in `SmartObject` interface. `SmartObject` interface will be mixed into the `SmartObject` class due to the [declaration merging](/docs/handbook/declaration-merging.html#merging-interfaces).
## Alternative Pattern

This treats the classes as interfaces, and only mixes the types behind Disposable and Activatable into the SmartObject type rather than the implementation. This means that we'll have to provide the implementation in class.
Except, that's exactly what we want to avoid by using mixins.
Previous versions of this document recommended a way to write mixins where you created both the runtime and type hierarchies separately, then merged them at the end:

Finally, we mix our mixins into the class implementation.
```ts twoslash
// @strict: false
// Each mixin is a traditional ES class
class Jumpable {
jump() {}
}

```ts
applyMixins(SmartObject, [Disposable, Activatable]);
```
class Duckable {
duck() {}
}

Lastly, we create a helper function that will do the mixing for us.
This will run through the properties of each of the mixins and copy them over to the target of the mixins, filling out the stand-in properties with their implementations.
// Including the base
class Sprite {
x = 0;
y = 0;
}

```ts
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach((baseCtor) => {
// Then you create an interface which merges
// the expected mixins with the same name as your base
interface Sprite extends Jumpable, Duckable {}
// Apply the mixins into the base class via
// the JS at runtime
applyMixins(Sprite, [Jumpable, Duckable]);

let player = new Sprite();
player.jump();
console.log(player.x, player.y);

// This can live anywhere in your codebase:
function applyMixins(derivedCtor: any, constructors: any[]) {
constructors.forEach((baseCtor) => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
Object.defineProperty(
derivedCtor.prototype,
Expand All @@ -120,3 +203,70 @@ function applyMixins(derivedCtor: any, baseCtors: any[]) {
});
}
```

This pattern relies less on the compiler, and more on your codebase to ensure both runtime and type-system are correctly kept in sync.

## Constraints

The mixin pattern is supported natively inside the TypeScript compiler by code flow analysis.
There are a few cases where you can hit the edges of the native support.

#### Decorators and Mixins [`#4881`](https://github.com/microsoft/TypeScript/issues/4881)

You cannot use decorators to provide mixins via code flow analysis:

```ts twoslash
// @experimentalDecorators
// @errors: 2339
// A decorator function which replicates the mixin pattern:
const Pausable = (target: typeof Player) => {
return class Pausable extends target {
shouldFreeze = false;
};
};

@Pausable
class Player {
x = 0;
y = 0;
}

// The Player class does not have the decorator's type merged:
const player = new Player();
player.shouldFreeze;

// It the runtime aspect could be manually replicated via
// type composition or interface merging.
type FreezablePlayer = typeof Player & { shouldFreeze: boolean };

const playerTwo = (new Player() as unknown) as FreezablePlayer;
playerTwo.shouldFreeze;
```

#### Static Property Mixins [`#17829`](https://github.com/microsoft/TypeScript/issues/17829)

More of a gotcha then a constraint.
The class expression pattern creates singletons, so they can't be mapped at the type system to support different variable types.

You can work around this by using functions to return your classes which differ based on a generic:

```ts twoslash
function base<T>() {
class Base {
static prop: T;
}
return Base;
}

function derived<T>() {
class Derived extends base<T>() {
static anotherProp: T;
}
return Derived;
}

class Spec extends derived<string>() {}

Spec.prop; // string
Spec.anotherProp; // string
```
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Constructor = new (...args: any[]) => {};
function Scale<TBase extends Constructor>(Base: TBase) {
return class extends Base {
// Mixins may not declare private/protected properties
// however, you can use ES2020 private fields
_scale = 1;

setScale(scale: number) {
Expand Down

0 comments on commit 618ad90

Please sign in to comment.