I have the following setup in TypeScript:
abstract class Test {
public abstract method1(param1: string): number;
public abstract method2(param1: number, param2: string): Promise<number>;
}
class Wow {
constructor(private test: Test) {}
public fetch<K extends keyof Test>(key: K, params: Parameters<Test[K]>): ReturnType<Test[K]> {
return this.test[key](params);
}
}
But the return line inside the method fetch gives two errors:
Type 'number | Promise' is not assignable to type 'ReturnType<Test[K]>'.
Type 'number' is not assignable to type 'ReturnType<Test[K]>'. (2322)
Expected 2 arguments, but got 1. (2554)
I'm puzzled on why these errors appear and how to solve them.
What I want is for my method fetch to take a specific key of my Test class and be strongly typed with the proper parameters and return value of method specified by the key.
TypeScript can't really do much reasoning about conditional types that depend on generic type parameters. The
Parameters<T>andReturnType<T>utility types are implemented as conditional types, and thusParameters<Test[K]>andReturnType<Test[K]>are essentially opaque to the compiler. The best it can do is to widenKto its constraint,keyof Test, and so you end up with a union of method names and a union of parameter lists. And then the compiler gets confused because it can't be sure thatthis.test[key]actually acceptsparamsas its parameter list, because maybe you're passing themethod1params tomethod2or vice versa. This is unlikely to actually happen (as long asKisn't specified with a union), but the compiler can't see that. It has lost track of the correlation betweenkeyandparams. The general issue here is TypeScript's lack of direct support for what I call "correlated unions", as discussed in microsoft/TypeScript#30581The recommended fix for that issue is described in microsoft/TypeScript#47109. The compiler is better about dealing with basic key-value interface types, and generic indexes into such types and mapped types over them.
For your example, it means that we need to rewrite
Testas such a mapped type. Like this:The
TestParamsandTestReturntypes are the "basic key-value interface types", andTestMappedthe mapped type over these types. You can see that the typeTestMappedis completely equivalent to the typeTest, and indeed the compiler will allow you to assign a value of typeTestto a variable of typeTestMapped.And now you can rewrite
fetch():Here the
paramsinput type and the return type are now written as generic indexes into our basic key-value interface types. Inside the implementation, we assignthis.testto a variablethisTestof typeTestMapped, which enables the compiler to "see" what we're doing when we callthisTest[key](...params). The type ofthisTest[key]is seen to be(...args: TestParams[K]) => TestReturn[K], and the type ofparamsis seen to beTestParams[K], so calling the former with a spread argument list of the latter produces a result of typeTestReturn[K], which is the desired output type of the function. So everything works.Note that this refactoring caught an error: your code was of the form
thisTest[key](params)instead ofthisTest[key](...params), meaning you passed the wholeparamsarray as the first argument instead of spreading it into multiple arguments. If you left it that way you'd get the compiler error:Argument of type '[TestParams[K]]' is not assignable to parameter of type 'TestParams[K]', which hopefully would be enough information for you to fix the problem.Now everything works as desired.
Playground link to code