Search code examples
typescriptfunctional-programmingdelegatesfunctor

TypeScript: How to pass function-type member variable by reference?


I have an object with a function-type member variable, and a setter function which sets any given function-type variable of that same signature to some function:

class Foo {
    // This is the function that we want to set to something else
    public delegated: (param : string) => void 
        = () => { console.log("Default value, was not set to anything else."); };
}

// This a function we should be able to set Foo.delegated to, as it has the same signature
function printAllCaps(s : string) : void {
    console.log(s.toUpperCase());
}

// This is the function that does the setting. Currently, it doesn't work.
function setDelegateToPrint(delegate : (param : string) => void) {
    delegate = printAllCaps;
}

let myFoo : Foo = new Foo();

// We try to set it -- no effect.
setDelegateToPrint(myFoo.delegated);

myFoo.delegated("hello!");

The setDelegateToPrint function has no effect. I assume that is because myFoo.delegatedFunction was not passed by-reference, but rather by-copy. Possibly because we didn't pass myFoo in some way.

How can this be done? It needs to be done without passing myFoo as a Foo, because the setter function should not know the Foo class definition for encapsulation. It can only know the bare minimum required information: there is an object, and the object has a member function of a given signature.

Note: this code not fullfilling the needed task can easily be checked by copy-pasting the entire code into TypeScript playground and pressing "run".


Solution

  • "This needs to be done without passing myFoo itself" - then, you can't.

    What you could do is pass myFoo, but make setDelegateToPrint generic and accept one of its keys.

    I changed a bit your code:

    // This class has not been changed
    class Foo {
      public delegatedFunction: (param: string) => void = () => {
        console.log("Default value, was not set to anything else.");
      };
    }
    
    // Here I'm just ensuring the function is the same type
    const printAllCaps: Foo["delegatedFunction"] = (s) => {
      console.log(s.toUpperCase());
    };
    
    // Here I added some type information so that the
    // second parameter has to be a key of the first parameter
    //
    // NOTE: this can be improved, for example passing a third
    // parameter which is the function (printAllCaps) and it
    // must be of the same type of the method you're overriding.
    // More on this later.
    function setDelegateToPrint<O, K extends keyof O>(
      obj: O[K] extends Foo["delegatedFunction"] ? O : never,
      key: K
    ) {
      obj[key] = printAllCaps;
    }
    
    let myFoo: Foo = new Foo();
    
    // Here I'm passing the key of the method to override, instead of passing the method itself
    setDelegateToPrint(myFoo, "delegatedFunction");
    
    myFoo.delegatedFunction("hello!");
    

    This can be improved by passing a third parameter to setDelegateToPrint and making it even more generic:

    function setDelegateToPrint<O, K extends keyof O>(
      obj: O,
      key: K,
      overrideFn: O[K]
    ) {
      obj[key] = overrideFn;
    }
    
    setDelegateToPrint(myFoo, "delegatedFunction", printAllCaps);
    

    FOOTNOTE:

    be aware that you're mutating an object from a scope outside the function and it is not advisable at all, it will be hard to debug for sure. Do this only if you're sure you cannot avoid that.