What you're seeing here is a behavior in TypeScript where control flow analysis does not narrow the type of a property when accessed using bracket notation (`obj[propName]`) instead of dot notation (`obj.xyz`). This is identified as an issue within TypeScript (refer to microsoft/TypeScript#10530) and has not been resolved due to performance concerns and safety considerations.
The typical workaround for this situation is to store the property value in a separate variable and then use control flow analysis to refine its type:
function call_IF_Function<T, K extends keyof T>(obj: T, propName: K) {
const objPropName = obj[propName];
if (typeof objPropName === "function") {
// `objPropName` type narrowed from `T[K]` to `T[K] & Function`
objPropName(); // valid function call
}
}
As shown above, `objPropName` is effectively narrowed from `T[K]` to `T[K] & Function`, making it callable.
Note that this approach is not completely type safe because the type `Function` allows calling with any arguments or none at all. Some functions may require specific argument types:
call_IF_Function({ oops: (s: string) => s.toUpperCase() }, "oops");
// Appears valid but results in a runtime error since 's' is undefined
To enhance type safety, you can constrain `T` so that function-valued properties do not expect arguments by modifying the generic constraints as follows:
function call_IF_Function<
T extends Record<K, (() => void) | string | number>,
K extends keyof T
>(obj: T, propName: K) {
const objPropName = obj[propName];
if (typeof objPropName === "function") {
// `objPropName` type once again narrowed to `T[K] & Function`
objPropName();
}
}
After applying the constraints above, you'll achieve more secure behavior:
call_IF_Function(x, "name"); // Executes without issues
call_IF_Function({ oops: (s: string) => s.toUpperCase() }, "oops"); // Error!
// -----------------------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Type '(s: string) => string' is not assignable to type '() => void'
For further experimentation, you can access the code on the Playground.