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 objects or factory functions. Here is my effort so far (with a little help from lodash):
class Barker {
constructor(private state) {}
bark() {
console.log(`Woof, I am ${this.state.name}`);
}
}
class Driver {
constructor(private state) {}
drive() {
this.state.position = this.state.position + this.state.speed;
}
}
class Killer {
constructor(private state) {}
kill() {
console.log(`Burn the ${this.state.prey}`);
}
}
class MurderRobotDog {
constructor(private state) {
return _.assignIn(
{},
new Killer(state),
new Driver(state),
new Barker(state)
);
}
}
const metalhead = new MurderRobotDog({
name: 'Metalhead',
position: 0,
speed: 100,
prey: 'witch'
});
metalhead.bark(); // expected: "Woof, I am Metalhead"
metalhead.kill(); // expected: "Burn the witch"
This resulting in:
TS2339: Property 'bark' does not exist on type 'MurderRobotDog'
TS2339: Property 'kill' does not exist on type 'MurderRobotDog'
What's the right way of doing class composition in TypeScript?
One more benefit of composition over inheritance is testing scope. Unit testing is easy in composition because we know what all methods we are using from another class. We can mock it up for testing whereas in inheritance we depend heavily on superclass and don't know what all methods of superclass will be used.
Mixins are a faux-multiple inheritance pattern for classes in JavaScript which TypeScript has support for. The pattern allows you to create a class which is a merge of many classes. To get started, we need a type which we'll use to extend other classes from.
You cannot extend multiple classes at once using the extends keyword in TypeScript. However, you can use a mixin — a special function that can be used for extending multiple classes.
An inherited derived class acquires the properties and behaviors of the base class. TypeScript supports single inheritance and multilevel inheritance. We can not implement hybrid and multiple inheritances using TypeScript.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With