typescripttypecheckingfunction-signature

TypeScript - check if object's property is a function with given signature


I have a function that gets a property from an object.

// Utils.ts
export function getProperty<T, K extends keyof T>(obj: T, key: string): T[K] {
    if (key in obj) { return obj[key as K]; }
    throw new Error(`Invalid object member "${key}"`);
}

I'd like to check if returned property is a function with given signature and then call the property with provided parameter.

getProperty() is used to dynamically get one of object's method and call it. I tried:

let property: this[keyof this] = utils.getProperty(this, name);
if (typeof property === 'function') ) { property(conf); }

But this gives "Cannot invoke an expression whose type lacks a call signature. Type 'any' has no compatible call signatures." error. I understand that a property that comes from getProperty() can indeed be of any type but how to ascertain it is a function with (conf: {}): void signature?


Solution

  • It doesn't look like typeof type guards exist for function types. It seems to be by design; see microsoft/TypeScript#2072 (that issue is about instanceof type guards but I'm guessing it's similar reasoning).

    However, TypeScript does have user-defined type guards, meaning you can write a function that narrows the type of its argument any way you like.


    TypeScript's static type system is erased when the code is compiled to JavaScript. At runtime, there is no such thing as (conf: {}) => void that you can examine. If you want to write a runtime test to distinguish values of type (conf: {}) => void from values of other types, you will only be able to go so far.

    You can test if typeof x === "function". But that just tells you it's a function. It doesn't indicate how many parameters the function takes and what their types are. You could also check if x.length === 1 to see if the function expects one argument, although there are caveats around Function.length involving rest parameters and default parameters. But now you're kind of stuck.

    At runtime, any information about function parameter types will have been erased, if those functions even came from TypeScript code in the first place. At most you could "probe" the function by calling it with some test parameters and checking to see if things break. But that's a destructive test with possible side effects, and defeats the purpose of checking a function is the right type before calling it.


    Maybe you are fine with this limitation and will just assume that a function whose length is 1 is a good enough test. Or maybe you have some other test (e.g., maybe you can add a property named isOfRightType whose value is true to all such functions you care about, and then just test for that property. This eliminates false positives by introducing the possibility of false negatives). Once you know your runtime test, you can make a type guard:

    function isFunctionOfRightType(x: any): x is (conf: {})=>void {
       return (typeof x === 'function') && (x.length === 1); // or whatever test
    }
    
    // ... later
    
    let property: this[keyof this] = utils.getProperty(this, name);
    if (isFunctionOfRightType(property)) { property(conf); } // no error now
    

    Playground link to code