16

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?

Community
  • 1
  • 1
Glenn Mohammad
  • 3,871
  • 4
  • 36
  • 45
  • 1
    I have no idea what `_.assignIn` is, but this looks much more like mixins than composition. And no, you cannot easily mix multiple `class`es – Bergi Feb 12 '18 at 23:20
  • @Bergi https://lodash.com/docs/4.17.5#assignIn. And yes, my effort goes around this answer over here: https://stackoverflow.com/a/39243098/2013891 - which suggests we should probably always prefer a mixin over member composition in prototypical language. ¯\_(ツ)_/¯ – Glenn Mohammad Feb 12 '18 at 23:36
  • 1
    MPJ got composition completely wrong, and actual composition never once made an appearance in his video. What you're looking for is called multiple inheritance or mixins. https://www.reddit.com/r/programming/comments/5dxq6i/composition_over_inheritance/da8bplv/ – Jeff M Feb 13 '18 at 15:06
  • @GlennMohammad ... but do you also know why a `MurderRobotDog` instance does not feature any of the expected (prototypal) methods? And / or did you already read the JavaScript code which your TS was transpiled to? – Peter Seliger Feb 14 '18 at 17:03
  • @JeffM Thanks for the great pointer! The thread really opens up my mind about what actual composition is. ❤︎ I always thought mixin is a form of composition. – Glenn Mohammad Feb 16 '18 at 18:42
  • @PeterSeliger I suppose it's because of the lodash's _.assignIn. :$ The methods are in the MurderRobotDog instance's object, but not in its prototype. And yes, I read it already. The method is correctly transpiled to the prototype of the classes to be composed (i.e. `Barker`, `Driver`, and `Killer`). – Glenn Mohammad Feb 16 '18 at 18:43

2 Answers2

13

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.

nomadoda
  • 4,561
  • 3
  • 30
  • 45
  • 5
    This is not at all what is meant by composition. The `implements` in typescript only ensures that a class conforms to a sub-type (e.g., has the variable `foo` or the function `bar`). Composition, on the other hand, does this as well but goes a step further by ensuring the class also conforms to implementation, i.e., composition **gives** the class the sub-type and implementation... e,g, we can ensure a class gets the variable `foo` with default value of 1. Typescript doesn't currently support this natively, but you can partially implement composition with mixins. – uɥƃnɐʌuop Oct 12 '19 at 19:01
3

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.

Ziggy
  • 21,845
  • 28
  • 75
  • 104
th3n3wguy
  • 3,649
  • 2
  • 23
  • 30
  • @th3n3wgy it looks like the github issue you pointed to is regarding interfaces. I couldn't find anything similar related to class composition – Nayfin Jul 09 '20 at 18:26