Reputation: 4020
I'm trying to create a FinalReturnType which extracts the final return type of a higher order function in Typescript:
type FinalReturnType = FinalReturnType<(a: string) => (b: string) => (c: string) => (d: string) => number>;
// expect FinalReturnType = number
I'm having issues because it circularly references itself.
I want to write this:
type FinalReturnType<F> =
F extends (...args: any[]) => infer R
? FinalReturnType<R>
: R;
Error:
Type alias 'FinalReturnType' circularly references itself.
And this is what I'm using at the moment - it's just a bit ugly and limited to four layers deep:
type FinalReturnType<A> = A extends (...args: any[]) => infer R1
? R1 extends (...args: any[]) => infer R2
? R2 extends (...args: any[]) => infer R3
? R3 extends (...args: any[]) => infer R4
? R4
: R3
: R2
: R1
: never;
I feel like this should be possible, but maybe it isn't?
Upvotes: 0
Views: 331
Reputation: 329678
Okay, expanding comments into answer:
Recursive conditional types like the one you want to make with FinalReturnType
are not officially supported. See microsoft/TypeScript#26980 for a suggestion to lift this constraint and allow such circular type definitions. The problem with supporting them, at least right now, is that the compiler generally evaluates types eagerly instead of deferring them (with a few exceptions); when the definition of the type is dependent on itself, the compiler would get stuck in a loop if it allowed such types. In practice what happens is that it either notices that there will be a loop and immediately warns you with a "type alias circularly references itself" error, or some other such error like "X is referenced directly or indirectly in its own type annotation", or it actually gets into the loop and gives you an error of the form "type instantiation is excessively deep and possibly infinite" after it reaches some internal recursion limit.
There are, for better or worse, ways to fool the compiler. They generally use features intended to allow some limited deferring of type evaluation. One such feature is that you can build recursively nested object types, like type LinkedList = {value: any, next?: LinkedList}
. The compiler defers the type evaluation when you push things down into object properties. This is explicitly intended to allow recursion, which is one piece of shoving your foot in the door. Combined with the ability to look up object property types by key, you have a dangerous tool.
Another such feature is when a conditional type depends on an unspecified generic type parameter, like type Foo<T> = T extends any ? true : false
. That type Foo<T>
will not evaluate to true
until you specify T
. Now if you could make such conditional types directly self-referential you'd be done, but the compiler tries to notice this and warns about it. But if you combine the this with the object nesting-and-lookup trick above, you get this:
type FinalReturnType<T> = {
0: T,
1: FinalReturnType<ReturnType<Extract<T, (...a: any) => any>>>
}[T extends (...a: any) => any ? 1 : 0]
Push the recursive bit into the 1
property, pull it out with a lookup, and make that lookup deferred by having it be dependent on an unresolved conditional type with no direct recursion in it. It works, too:
type Str = FinalReturnType<() => () => () => () => () => string> // string
It is not supported. It is against the rules. It is not guaranteed to work in all cases.
For example, consider this:
interface Add { (val: number): Add, val: number };
function a(this: { val: number }, x: number): Add {
return Object.assign(a.bind({ val: x + this.val }), { val: x + this.val });
}
const add: Add = a.bind({ val: 0 })(0);
console.log(add(1)(2)(3).val); // 6
console.log(add(4)(5)(6).val); // 15
What's the final return type of Add
? Let's see:
type Whoopsie = FinalReturnType<Add>; // any
Well, okay, maybe that's fine... uh oh, look back up at FinalReturnType
's definition:
type FinalReturnType<T> = {
0: T,
1: FinalReturnType<ReturnType<Extract<T, (...a: any) => any>>> // error!
//~ <--- '1' is referenced directly or indirectly in its own type annotation
}[T extends (...a: any) => any ? 1 : 0];
There's an error now.
This issue is certainly patchable. One thing I've done before is write my own recursion limiter, like this:
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
type FinalReturnType<T, N extends number = 15> =
0 extends N ? T : {
0: T, 1: FinalReturnType<ReturnType<Extract<T, (...a: any) => any>>, Prev[N]>
}[T extends (...a: any) => any ? 1 : 0]
type Str = FinalReturnType<() => () => () => () => () => string> // string
type OkayNow = FinalReturnType<Add>; // Add
Where FinalReturnType<T, N>
takes a type T
and a recursion limit N
and bails out by returning just T
if you recurse too far. As long as N
is kept smaller than the nebulous internal compiler recursion limit, it should work, maybe, possibly. It's still not officially supported.
Other people have other ways of convincing the compiler into supporting arbitrary-depth recursive type analysis. There's a ts-toolbelt library that may soon make it into TypeScript's test cases; once that happens, you might be able to use something from that library, since we'll at least expect it not to break when the language is updated. But for now it's not in there, so I can't advise you to do that.
Normally what I do in these cases is build something that works up to some reasonable max depth, sort of like what you've already done up to depth 4. That type might be ugly, but it's straightforward, and it does what you've asked it to do. You might be able to rewrite it in some less ugly-looking way, but the basic issue is still there: circular conditional types are not currently directly supported.
Okay, hope that helps; good luck!
Upvotes: 4