When you utilize declaration merging, you are essentially informing the compiler about the functionality of your mixins since it cannot deduce that on its own. The documentation refers to this practice as an "alternative pattern", emphasizing that "this pattern relies less on the compiler, and more on your codebase to ensure both runtime and type-system are correctly kept in sync."
Therefore, be prepared for some additional complexity in persuading the compiler that your seemingly independent classes like Jumpable
and Duckable
can access properties such as x
and y
which are not explicitly declared within them. One straightforward approach is defining an interface representing the desired structure:
interface Positioned {
x: number,
y: number
}
You can then specify that the jump()
and duck()
methods should function on objects adhering to that structure by utilizing a this
parameter:
class Jumpable {
jump(this: Positioned) {
console.log("jump");
this.y += 2;
}
}
class Duckable {
duck(this: Positioned) {
console.log("duck");
this.y -= 1;
}
}
This setup allows for modifying x
and y
inside the method while also warning against using the mixin methods incorrectly as regular methods:
const j = new Jumpable();
j.jump(); // compiler error
// The 'this' context of type 'Jumpable' is not
// assignable to method's 'this' of type 'Positioned'.
After implementing these changes, you'll observe seamless functionality:
class Sprite {
name = "";
x = 0;
y = 0;
constructor(name: string) {
this.name = name;
}
}
interface Sprite extends Jumpable, Duckable { }
applyMixins(Sprite, [Jumpable, Duckable]);
const player = new Sprite("Player");
console.log(player.name, player.x, player.y); // Player 0 0
player.jump(); // jump
console.log(player.name, player.x, player.y); // Player 0 2
player.duck(); // duck
console.log(player.name, player.x, player.y); // Player 0 1
Hence, if you opt for the alternative pattern, you will have a smooth experience.
The recommended non-alternative mixin approach involves utilizing class factory functions that leverage standard class inheritance to extend a base class with the mixin functionalities. By restricting the base class to constructors of Positioned
objects, the mixins gain access to the base class's x
and y
properties:
function Jumpable<TBase extends new (...args: any[]) => Positioned>(Base: TBase) {
return class Jumpable extends Base {
jump() {
console.log("jump");
this.y += 2;
}
};
}
function Duckable<TBase extends new (...args: any[]) => Positioned>(Base: TBase) {
return class Duckable extends Base {
duck() {
console.log("duck");
this.y -= 1;
}
};
}
The class expressions within the Jumpable
and Duckable
factory functions enable jump()
and duck()
to interact with this.y
, given that the Base
constructor aligns with type TBase
, known for generating a subtype of Positioned
.
For applying mixin methods to prototypes effortlessly, simply invoke the mixin factory functions on constructors:
class BaseSprite {
name = "";
x = 0;
y = 0;
constructor(name: string) {
this.name = name;
}
}
const Sprite = Jumpable(Duckable(BaseSprite));
No declaration merging is required here; the compiler automatically grasps that instances of Sprite
exhibit traits of Positioned
alongside possessing the methods jump()
and duck()
:
const player = new Sprite("Player");
console.log(player.name, player.x, player.y); // Player 0 0
player.jump(); // jump
console.log(player.name, player.x, player.y); // Player 0 2
player.duck(); // duck
console.log(player.name, player.x, player.y); // Player 0 1
Link to Play Code