The issue lies in the fact that the compiler is unable to recognize that arg
and bar[fn]
are interconnected. Instead, it treats both of them as independent union types, leading to an expectation that all combinations of union constituents are valid, even though most combinations are not.
In TypeScript 3.2, you would have received an error message stating that bar[fn]
does not possess a call signature due to being a union of functions with distinct parameters. It's unlikely that this code functioned correctly in TS2.6; especially since the use of Parameters<>
was not present until conditional types were introduced in TS2.8. I attempted to refactor your code to be compatible with TS2.6 using:
interface B {
foo: MyNumberType,
bar: MyStringType,
baz:MyBooleanType
}
function test<T extends keyof Bar>(bar: Bar, fn: T) {
let arg: B[T]=null!
bar[fn](arg); // this line triggers an error
}
I also tested this code in TS2.7 which still resulted in an error. Thus, it seems that the original code never functioned properly.
Regarding the never
complication: TypeScript 3.3 brought about enhanced support for calling unions of functions. This required the parameters to represent the intersection of parameters from the union of functions. While this is beneficial in some scenarios, in your case, the parameter must signify the intersection of several unique string literals, resulting in collapsing down to never
, essentially reflecting the same "cannot call this" error but in a more complex manner.
To address this effectively, one solution is to utilize a type assertion, as you possess more insight than the compiler in this particular instance:
function test<T extends keyof Bar>(bar: Bar, fn: T) {
let arg: Parameters<Bar[T]>[0] = null!; // assign a value
// Assert that bar[fn] accepts a union of arguments and returns a union of results
(bar[fn] as (x: typeof arg) => ReturnType<Bar[T]>)(arg); // no error
}
A type assertion does introduce risk, allowing you to mislead the compiler:
function evilTest<T extends keyof Bar>(bar: Bar, fn: T) {
// The assertion below deceives the compiler
(bar[fn] as (x: Parameters<Bar[T]>[0]) => ReturnType<Bar[T]>)("up"); // no error!
}
Therefore, caution should be exercised. While there exists a method to create a completely type-safe version, involving coercing the compiler into executing code flow analysis on every potentiality:
function manualTest<T extends keyof Bar>(bar: Bar, fn: T): ReturnType<Bar[T]>;
// Unions can be narrowed, generics cannot
// Refer to https://github.com/Microsoft/TypeScript/issues/13995
// and https://github.com/microsoft/TypeScript/issues/24085
function manualTest(bar: Bar, fn: keyof Bar) {
switch (fn) {
case 'foo': {
let arg: Parameters<Bar[typeof fn]>[0] = null!
return bar[fn](arg);
}
case 'bar': {
let arg: Parameters<Bar[typeof fn]>[0] = null!
return bar[fn](arg);
}
case 'baz': {
let arg: Parameters<Bar[typeof fn]>[0] = null!
return bar[fn](arg);
}
default:
return assertUnreachable(fn);
}
}
Although this approach remains fragile (necessitating modifications if additional methods are appended to Bar
) and repetitive (repeating identical clauses), I typically lean towards the type assertion technique discussed earlier.
Hopefully, this information proves helpful; best of luck!