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

14.4k Views Asked by At

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?

2

There are 2 best solutions below

0
On BEST ANSWER

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

0
On

In you function signature there is a mistake, the key parameter should be of type K and this will give you better inference when you use constants for parameters:

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}"`);
}

let foo = {
    fn (conf: {}): void {}
}
let fn = getProperty(foo, "fn");
fn({}); // callable 

The problem when you use a key which is a string and is not validated at compile time is that the compiler can't really help you with anything. It will asume that since you index my an arbitrary string the return type can be any valid field type of the target. There is no way to validate function parameter types at runtime as they will be erased, you can validate parameter count though:

let property:  Function = getProperty(this, name) as any;
if (typeof property === 'function' && property.length == 1)  
{ 
    property({}); 
}