Based on this awesome Composition over Inheritance video by MPJ, I\'ve been trying to formulate composition in TypeScript. I want to compose classes, not object
Unfortunately, there is no easy way to do this. There is currently a proposal to allow for the extends
keyword to allow you to do this, but it is still being talked about in this GitHub issue.
Your only other option is to use the Mixins functionality available in TypeScript, but the problem with that approach is that you have to re-define each function or method that you want to re-use from the "inherited" classes.
I think we should make a distinction between composition and inheritance and reconsider what we are trying to achieve. As a commenter pointed out, what MPJ does is actually an example of using mixins. This is basically a form of inheritance, adding implementation on the target object (mixing).
I tried to come up with a neat way to do this and this is my best suggestion:
type Constructor<I extends Base> = new (...args: any[]) => I;
class Base {}
function Flies<T extends Constructor<Base>>(constructor: T = Base as any) {
return class extends constructor implements IFlies {
public fly() {
console.log("Hi, I fly!");
}
};
}
function Quacks<T extends Constructor<Base>>(constructor: T = Base as any) {
return class extends constructor implements ICanQuack {
public quack(this: IHasSound, loud: boolean) {
console.log(loud ? this.sound.toUpperCase() : this.sound);
}
};
}
interface IHasSound {
sound: string;
}
interface ICanQuack {
quack(loud: boolean): void;
}
interface IQuacks extends IHasSound, ICanQuack {}
interface IFlies {
fly(): void;
}
class MonsterDuck extends Quacks(Flies()) implements IQuacks, IFlies {
public sound = "quackly!!!";
}
class RubberDuck extends Quacks() implements IQuacks {
public sound = "quack";
}
const monsterDuck = new MonsterDuck();
monsterDuck.quack(true); // "QUACKLY!!!"
monsterDuck.fly(); // "Hi, I fly!"
const rubberDuck = new RubberDuck();
rubberDuck.quack(false); // "quack"
The benefit of using this approach is that you can allow access to certain properties of the owner object in the implementation of the inherited methods. Although a bit better naming could be use, I see this as a very potential solution.
Composition is instead of mixing the functions into the object, we set what behaviours should be contained in it instead, and then implement these as self-contained libraries inside the object.
interface IQuackBehaviour {
quack(): void;
}
interface IFlyBehaviour {
fly(): void;
}
class NormalQuack implements IQuackBehaviour {
public quack() {
console.log("quack");
}
}
class MonsterQuack implements IQuackBehaviour {
public quack() {
console.log("QUACK!!!");
}
}
class FlyWithWings implements IFlyBehaviour {
public fly() {
console.log("I am flying with wings");
}
}
class CannotFly implements IFlyBehaviour {
public fly() {
console.log("Sorry! Cannot fly");
}
}
interface IDuck {
flyBehaviour: IFlyBehaviour;
quackBehaviour: IQuackBehaviour;
}
class MonsterDuck implements IDuck {
constructor(
public flyBehaviour = new FlyWithWings(),
public quackBehaviour = new MonsterQuack()
) {}
}
class RubberDuck implements IDuck {
constructor(
public flyBehaviour = new CannotFly(),
public quackBehaviour = new NormalQuack()
) {}
}
const monsterDuck = new MonsterDuck();
monsterDuck.quackBehaviour.quack(); // "QUACK!!!"
monsterDuck.flyBehaviour.fly(); // "I am flying with wings"
const rubberDuck = new RubberDuck();
rubberDuck.quackBehaviour.quack(); // "quack"
As you can see, the practical difference is that the composites doesn't know of any properties existing on the object using it. This is probably a good thing, as it conforms to the principle of Composition over Inheritance.