To tailor the specific instance illustrated, you have the option to manually define the desired type as follows:
type TestTypeManual<T, R> = (t1: T, t2: R) => T | R;
type TestTypeReturnManual = ReturnType<TestTypeManual<number, number>>; // number
However, if you aim to delegate this task to the compiler.
The TypeScript's type system does not possess enough complexity to explain the connection between a concrete type alias representing a generic function like
type GenFunc = <T>(x: T)=>T
and a generic type alias representing a particular function type like
type GenAlias<T> = (x:T)=>T
. It is impossible to specify the type parameter(s) of a generic function without actually calling it. For example, although you can invoke a function of type
GenFunc
using
genFunc<string>("hello")
, there exists no such
type as
GenFunc<string>
. Check out this
suggestion thread (microsoft/TypeScript#17574) that might introduce higher rank type manipulation in the future.
As of now, achieving this purely at the type level seems implausible.
If you are willing to explore some unconventional methods that may impact the generated JavaScript code, you could utilize the ability to infer generic functions introduced in TypeScript 3.4. Here is a potential approach. First, create a function declaration similar to:
// you could just declare this instead of implementing it, but whatever:
function magicThunk<A extends any[], R>(f: (...args: A) => R): () => (...args: A) => R {
return () => f;
}
The purpose of the magicThunk()
function is to take a callback function f
and return a new zero-argument function that returns f
. Surprisingly, when you call magicThunk()
with a generic function on TS3.4:
const genericIdentity = <T>(x: T) => x; // <T>(x: T) => T
const thunkIdentity = magicThunk(genericIdentity); // <T>() => (x: T) => T
The returned function also becomes generic, but the generic type parameters are shifted one level outward. Consequently, you can call the thunked generic function and specify its type parameters to obtain a non-generic function:
const stringIdentity = thunkIdentity<string>(); // (x: string) => string;
Thus, this allows you to "specify the type parameters of a generic function without calling it," which aligns with your objective.
Almost there... the final step involves creating a new generic class
to introduce certain type parameters into scope without directly placing them on another function. Consider this implementation for your Test["open"]
type:
class TransformTest<T, R> {
open = magicThunk(new Test().open)<T, R>()
}
Subsequently, the type of TransformTest<T, R>.open
should reflect the desired outcome:
type TestType<T, R> = TransformTest<T, R>["open"]; // (t1: T, t2: R) => T | R
type TestTypeReturn = ReturnType<TestType<number, number>>; // number
This method produces the expected result. Note that there is no requirement to execute magicThunk()
or instantiate TransformTest
within your code. The compiler performs all necessary operations during design time, simulating the evaluation of emitted code types accordingly.
Hopefully, this explanation proves helpful. Best of luck!
Link to code