Search code examples
typescriptthisgetter-setter

How to combine values returned by getters on two classes without losing this context (as a single type)


I have three classes, Chat, Quest and Receiver.

It has three actions (methods) that are needed by another class, i.e. Class Quest. To avoid tightly coupling the two, I defined a getter (i.e. chatInterface) on class Chat to expose the three methods and pass them to class Receiver. I also used bind to avoid losing the this context.

Two of the actions (actionB and actionC) were moved to a third class, Quest. They're also exposed by a different getter in class Quest.

Is there a way I can combine the three methods (one method returned by class Chat, and 2 methods returned by class Quest), and pass them as a single type to class Receiver, without losing this context.

Here is a minimal reproducible example:

type ChatInterface = {
  actionA(): void;
}

type QuestInterface = {
  actionB(): void;
  actionC(): void;
}

class Chat {
  readonly quest: Quest = new Quest();
  readonly receiver: Receiver = new Receiver(this.chatInterface, this.quest.questInterface);
  
  get chatInterface(): ChatInterface{
    return {
      actionA: this.actionA.bind(this),
    }
  }

  actionA() {

  }
}

class Quest{
  get questInterface(): QuestInterface{
    return {
      actionB: this.actionB.bind(this),
      actionC: this.actionC.bind(this),
    }
  }
  actionB() {
    
  }

  actionC() {
    
  }
}

class Receiver {
  constructor(private readonly chatInterface: ChatInterface, private readonly questInterface: QuestInterface){}
}


Solution

  • A single object type with all the properties from both object type A and object type B would the intersection of A and B, which is expressed in TypeScript as A & B.

    (Aside: things get tricky if A and B have overlapping or conflicting property keys, but this doesn't happen in your examples. So it would be a digression to go into all the weird things that could happen, and I will ignore that possibility in what follows.)

    Conceptually therefore all you'd have to do is take the Receiver constructor and collapse the two parameters of type ChatInterface and QuestInterface into a single parameter of type ChatInterface & QuestInterface:

    class Receiver {
      constructor(private readonly chatAndQuest: ChatInterface & QuestInterface) {
      }
      callActions() {
        this.chatAndQuest.actionA()
        this.chatAndQuest.actionB()
        this.chatAndQuest.actionC()
      }
    }
    

    Of course that means you have to change the constructor call from new Receiver(this.chatInterface, this.questInterface) to something else. The easiest way to do this would be to spread both this.chatInterface and this.questInterface into a new object:

        new Receiver({ ...this.chatInterface, ...this.quest.questInterface });
    

    Normally you need to be careful when copying method-like properties from one object to another because of issues with the this context. But in your case you've already used bind() to fix the this context, so the particular object holding these methods no longer matters.

    So this should work as expected. If each actionX method calls console.log("X", this) where X is A or B or C, then you can verify that Receiver has everything it needs:

    const c = new Chat();
    c.receiver.callActions();
    
    /* 
    "A",  Chat: {
      "quest": {},
      "receiver": {
        "chatAndQuest": {}
      }
    } 
    "B",  Quest: {} 
    "C",  Quest: {}  
    */
    

    Playground link to code