By using declaration merging you are manually telling the compiler what your mixins are doing, because it can't figure that out itself. The documentation calls what you're doing an "alternative pattern", and 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."
So you should expect some additional tedium involved in convincing the compiler that your seemingly-independent Jumpable and Duckable classes can access x and y properties that aren't declared in them. Perhaps the easiest way to do this is to define an interface for something with the right shape:
interface Positioned {
x: number,
y: number
}
And then tell the compiler that the jump() and duck() methods are meant to operate on something of that shape, by using 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 both lets you modify x and y inside the method, and also warns you if you try to use the mixin methods as if they were 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'.
Once you do that, you can demonstrate that things just "work":
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
So that's great, if you're going to use the alternative pattern.
The recommended, non-alternative mixin pattern is to use class factory functions, that use standard class inheritance to extend a base class with the mixin. And by constraining the base class to constructors of Positioned objects, you can give the mixin 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 inside the Jumpable and Duckable factory functions allow jump() and duck() to access this.y, because the Base constructor is of type TBase, which is known to construct some subtype of Positioned.
And now instead of manually applying the mixin methods to prototypes, you can just call the mixin factory functions on constructors:
class BaseSprite {
name = "";
x = 0;
y = 0;
constructor(name: string) {
this.name = name;
}
}
const Sprite = Jumpable(Duckable(BaseSprite));
And note that no declaration merging is necessary; the compiler will automatically understand that Sprite instances are Positioned but also have jump() and duck() methods:
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
Playground link to code