Search code examples
typescripttype-safety

How can I get type safety for returned functions in TypeScript?


This TypeScript compiles fine:

abstract class Animal {
    /*
    Any extension of Animal MUST have a function which returns
    another function that has exactly the signature (string): void
     */
    abstract getPlayBehavior(): (toy: string) => void;
}

class Cat extends Animal {
    /*
    Clearly does not have a function which returns a function
    that has the correct signature. This function returns a function with
    the signature (void) : void
    */
    getPlayBehavior() {
        return () => {
            console.log(`Play with toy_var_would_go_here!`);
        };
    }
}

class Program {
    static main() {
        let cat: Animal = new Cat();
        cat.getPlayBehavior()("Toy");
    }
}

Program.main();

I am expecting an error because the Cat class definitely does not implement the abstract Animal class properly. I expect that the Cat class must have a function which returns another function of the exact signature specified in the abstract Animal class.

Running the code, I get:

> node index.js
> Play with toy_var_would_go_here!

Is there anything I can do to make sure the compiler enforces this kind of policy?


Solution

  • You're not getting an error because in javascript/typescript you're not forced to declare the arguments if you don't want to use them, as long as there's no contradiction.

    For example, the signature of the Array.forEach is:

    forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void;
    

    But this will compile just fine:

    let a = [1, 2, 3];
    a.forEach(item => console.log(item));
    

    And that's a good thing, it would have been horrible if I had to have all arguments even if I don't use them.
    The same goes here:

    type MyFn = (s: string) => void;
    let fn: MyFn = () => console.log("hey");
    

    If I don't need to use the string argument then I can neglect it, or I can even do:

    let fn: MyFn = () => console.log(arguments);
    

    If you'll change the signature of the function that you return in Cat.getPlayBehavior to something that contradicts the definition in Animal then you'll get an error:

    class Cat extends Animal {
        getPlayBehavior() {
            return (n: number) => {
                console.log(`Play with toy_var_would_go_here!`);
            };
        }
    }
    

    Error:

    Class 'Cat' incorrectly extends base class 'Animal'.
      Types of property 'getPlayBehavior' are incompatible.
        Type '() => (n: number) => void' is not assignable to type '() => (toy: string) => void'.
          Type '(n: number) => void' is not assignable to type '(toy: string) => void'.
            Types of parameters 'n' and 'toy' are incompatible.
              Type 'string' is not assignable to type 'number'.