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?
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.
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.
Related
For example, using mix-ins to extends multiple utility classes, like this:
import { autorun, makeObservable } from "mobx";
type GConstructor<T = {}> = new (...args: any[]) => T;
interface HasLife {
grow: () => void;
}
class Animal {
age: number;
constructor(age: number) {
this.age = age;
}
}
function addLife<TBase extends GConstructor<Animal>>(Base: TBase) {
return class Jumpable extends Base {
// this is wrong, mixins class cannot use constructor!
constructor() {
super();
makeObservable(this, {
age: observable,
grow: action,
});
}
grow() {
this.age++;
}
};
}
class Dog extends addLife(Animal) implements HasLife {
constructor(age: number) {
super(age);
}
}
const dog = new Dog(0);
autorun(() => {
console.log(dog.age);
});
dog.grow();
The example I provided cannot run correctly because mixin classes cannot have constructors, but I would also like to mark the member methods as observable or action in the mixin classes.
What should I do?
or, What is the common approach within the mobx community for dealing with this problem using other methods?
I'm trying to implement the runtime decorator pattern (not to be confused with the other kind of decorator, though they could be used the implementation) generically. For example, in this example, BoomThingDecorator is an actual concrete decorator and ThingDecorator is a convenience base class defined to not have to repeat the forwarding declarations for every decorator for this type. Instead of repeating the forward declarations for every class we want to decorate we can generically create such a base decorator class for any interface. I'm not sure if this is possible since TypeScript interfaces don't exist at runtime.
interface IThing {
run(): void;
}
class Thing implements IThing {
run(): void {
null;
}
}
abstract class ThingDecorator implements IThing {
protected base: IThing;
constructor(base: IThing) {
this.base = base;
}
run(): void {
return this.base.run();
}
}
class BoomThingDecorator extends ThingDecorator {
boom(): number {
return 1;
}
}
e.g. something like
abstract class Decorator<T> {
protected base: T;
constructor(base: T) { this.base = base; }
// something to forward all the methods of base
}
class BoomThingDecorator extends Decorator<IThing> {
// decorator methods
}
I have started a restaurant and created software the determine the price of all the items on the menu
I started by creating a class for each menu item so that the price can be calculated using an interface.
interface HasPrice {
getPrice(): number;
}
class Ramen implements HasPrice {
getPrice() {
return 5;
}
}
class Spaghetti implements HasPrice {
getPrice() {
return 10;
}
}
She then decided there should be topping so she used the decorator pattern.
class RamenWithPork extends Ramen {
getPrice() {
super.getPrice() + 3;
}
}
This worked until I decided to expand the topping menu and it became too cumbersome to deal combinatorial amount of classes. How should I fix it?
Decorator pattern
OK, so this is the job of the decorator design pattern - you can wrap your objects in another that decorates them with extra behaviour. This solves the problem of convoluted dependency hierarchies. You've already experienced it but just to illustrate it - let's say you have
class Rament {}
class RamenWithPork extends Ramen {}
class RamenWithChicken extends Ramen {}
class RamenWithMushrooms extends Ramen {}
What happens if you have RamenWithChickenAndMushroom? Do you extend RamenWithChicken or RamenWithMushrooms or just Ramen? What if you have RamenWithPorkMushroomsAndChicken? First of all, can you even remember the name of this class - if you sit down a day later, can you remember if it was pork, chicken and mushrooms or pork, mushrooms and chicken? Second how do you model that in the class hierarchy?
Here is where the decorator pattern comes in - you can create decorators like:
interface OrderAddition {}
class WithChicken implements OrderAddition {}
class WithMushrooms implements OrderAddition {}
class WithPork implements OrderAddition {}
Which can then be used with any food. The interface Extras is here for convenience. It helps keep the dependency hierarchy focused and more practical - HasPrice is a bit too vague - anything can have a price, but with OrderAddition we know exactly what kind this is. Another way to call it is OrderDecorator - it's a bit drier but it's more descriptive and immediately calls out the design pattern used.
Using classes
So, here is what your code can look like with decorators:
interface MainDish { //renaming it to be more descriptive
getPrice(): number;
}
class Ramen implements MainDish {
getPrice() {
return 5;
}
}
class Spaghetti implements MainDish {
getPrice() {
return 10;
}
}
//decorators would need to do the same as the object they decorate,
//so the decorator implements the MainDish interface
interface OrderAddition extends MainDish {}
abstract class AbstractOrderAddition implements OrderAddition {
protected base: MainDish;
constructor(base: MainDish) {
this.base = base;
}
abstract getPrice(): number;
}
class WithChicken extends AbstractOrderAddition {
getPrice() {
return this.base.getPrice() + 5
}
}
class WithMushrooms extends AbstractOrderAddition {
getPrice() {
return this.base.getPrice() + 1
}
}
class WithPork extends AbstractOrderAddition {
getPrice() {
return this.base.getPrice() + 3
}
}
Which then allows you to do the following:
let order: MainDish = new Ramen();
order = new WithPork(order);
order = new WithMushrooms(order);
Check on the TypeScript Playground
Using decorator functions
This, however, is highly formal usage of decorators. In JavaScript, you can short-circuit some of this and still maintain the same idea. Instead of having classes you can have your decorators as functions that modify the instance they are given:
interface OrderAdditionMaker{
(additionalPrice: number) : AddToOrder
}
interface AddToOrder {
(base: MainDish) : MainDish
}
const withChicken: AddToOrder = function withChicken(base: MainDish): MainDish {
//take a reference of the original
const originalGetPrice: MainDish["getPrice"] = base.getPrice.bind(base);
//overwrite the `getPrice` method
base.getPrice = function(): ReturnType<MainDish["getPrice"]> {
return originalGetPrice() + 5;
}
return base;
}
This can be further generalised into a more functional route by using currying so you can easily derive all the decorator functions:
//interface for making decorators
interface OrderAdditionMaker{
(additionalPrice: number) : AddToOrder
}
//decorator function
interface AddToOrder {
(base: MainDish) : MainDish
}
const withExtra: OrderAdditionMaker = function (additionalPrice: number) {
return function (base: MainDish): MainDish {
const originalGetPrice: MainDish["getPrice"] = base.getPrice.bind(base);
base.getPrice = function (): ReturnType<MainDish["getPrice"]> {
return originalGetPrice() + additionalPrice;
}
return base;
}
}
const withChicken: AddToOrder = withExtra(5);
const withMushrooms: AddToOrder = withExtra(1);
const withPork: AddToOrder = withExtra(3);
Check on the TypeScript Playground
I need to extend the two classes from the same namespace.
for ex:
declare namespace myNameSpace{
class class1{
///some methods will be here
}
class class3 extends class1{
//some method wil be here
}
class class2 extends myNameSpace. class3 {
//some methods will be here
}
export namespace class2 {
//declaration will be here
}
}
i need to extend the 'myNameSpace.class1' class as well as 'class2' namespace.
class newClass extends myNameSpace.class1, myNameSpace.class2 {
constructor() {
super();
}
}
If i call the both the classes, i got an error message
classes can only extend a single class
Is there any other way to fix this issue in typescript.
Is there any other way to fix this issue in typescript.
TypeScript is single inheritance by design.
You can use mixins but you can't override methods (unless you write a custom applyMixins methods)
Using the method:
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
You have to implement (on empty way)
class NewClass implements myNameSpace.class1, myNameSpace.class2 {
// empty implementation
public methodFrom1 : ()=>void;
public methodFrom2 : ()=>number;
constructor() {
// no super()
}
}
now use mixing to actually make it multi extend classes:
applyMixins(NewClass, [myNameSpace.class1, myNameSpace.class2]);
and now you can create the class
const foo = new NewClass()
foo.methodFrom1() // actually calls nameSpace.class1.prototype.methodFrom1
I'm trying to inherit base interface IScreen which is going to be extended with IModal, IEmbedded or ITab, and lock down methods so that they are specific to an interface. But what happens is that the method public OpenModal(modal: IModal) just accepts any type for some reason.
Playground
namespace Framework {
"use strict";
interface IScreen { }
interface IModal extends IScreen { }
interface ITab extends IScreen { }
interface IEmbedded extends IScreen { }
class BaseScreen implements IScreen {
public HandleCloseEvent() {
}
}
class DetailsScreen extends BaseScreen implements IModal {
}
class ListScreen extends BaseScreen implements IEmbedded {
}
class OpenerService {
public OpenScreen(screen: IScreen) {
}
public OpenModal(modal: IModal) {
}
}
class Controller {
constructor(openerService: OpenerService) {
var detailsScreen: DetailsScreen = new DetailsScreen();
var listScreen: ListScreen = new ListScreen();
openerService.OpenModal(212121); // Expected error
openerService.OpenModal(listScreen); // Expected error
}
}
}
Am I missing some compiler configurations that I am not
receiving any errors?
Am I misunderstanding how interfaces work?
It is the way TypeScript works: with structural typing (called also duck typing).
You declares:
public OpenModal(modal: IModal) {
}
Because all your interfaces are empty, it is equivalent to write:
public OpenModal(modal: {}) {
}
This signature accepts every object, because all objects are compatible to an empty object. 212121 is an object (in JavaScript a number is an object). listScreen is an object too.
You can notice it is useless to declare several interfaces for the empty object type:
interface IScreen { }
interface IModal extends IScreen { }
interface ITab extends IScreen { }
interface IEmbedded extends IScreen { }
Here, all your interfaces are equivalent. There is no hierarchy. You can do:
let a: IScreen
let b: IModal
let c: IModal = a // type '{}' ('IScreen') is compatible to type '{}' ('IModal')
let d = {}
let e: IModal = d // type '{}' is compatible to type '{}' ('IModal')
See also:
What is structural typing?
The Handbook on interfaces
Types are structural