TypeScript: class composition

Glenn Mohammad picture Glenn Mohammad · Feb 13, 2018 · Viewed 9.2k times · Source

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?

Answer

nomadoda picture nomadoda · Apr 9, 2019

Composition vs Inheritance

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).

Multiple inheritance

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

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.