Repository navigation
Recursive conditional types are aliased #22575
Copy link
Copy link
Open
Labels
Experience EnhancementNoncontroversial enhancementsNoncontroversial enhancementsSuggestionAn idea for TypeScriptAn idea for TypeScript
Milestone
Description
I am trying to create a conditional type that converts
type Before = {
a: string;
b: number;
c: string | undefined;
d: number | undefined;
nested: {
a2: string;
b2: number;
c2: string | undefined;
d2: number | undefined;
nested2: {
a3: string;
b3: number;
c3: string | undefined;
d3: number | undefined;
};
};
};to
type After = {
a: string;
b: number;
c?: string | undefined;
d?: number | undefined;
nested: {
a2: string;
b2: number;
c2?: string | undefined;
d2?: number | undefined;
nested2: {
a3: string;
b3: number;
c3?: string | undefined;
d3?: number | undefined;
};
};
};When I hover on After in the below code
const fnBefore = (input: Before) => {
return input;
};
const fnAfter = (input: After) => {
return input;
};it shows
type After = {
c?: string | undefined;
d?: number | undefined;
a: string;
b: number;
nested: Flatten<{
c2?: string | undefined;
d2?: number | undefined;
} & {
a2: string;
b2: number;
nested2: Flatten<{
c3?: string | undefined;
d3?: number | undefined;
} & RequiredProps>;
}>;
}instead of properly converted type.
According to #22011
If an conditional type is instantiated over 100 times, we consider that to be too deep.
At that point, we try to find the respective alias type that contains that conditional type.
only more complex types should be aliased, but I always face this issue.
Also for simple types:
type Simple = {
nested: {
a2: string;
c2: string | undefined;
};
};TypeScript Version: 2.8.0-dev.201180314
Full Code
type Before = {
a: string;
b: number;
c: string | undefined;
d: number | undefined;
nested: {
a2: string;
b2: number;
c2: string | undefined;
d2: number | undefined;
nested2: {
a3: string;
b3: number;
c3: string | undefined;
d3: number | undefined;
};
};
};
type Simple = {
nested: {
a2: string;
c2: string | undefined;
};
};
type Flatten<T> = { [K in keyof T]: T[K] };
type OptionalPropNames<T> = { [P in keyof T]: undefined extends T[P] ? P : never }[keyof T];
type RequiredPropNames<T> = { [P in keyof T]: undefined extends T[P] ? never : P }[keyof T];
type OptionalProps<T> = { [P in OptionalPropNames<T>]: T[P] };
type RequiredProps<T> = { [P in RequiredPropNames<T>]: T[P] };
type MakeOptional<T> = { [P in keyof T]?: T[P] };
type ConvertObject<T> = Flatten<MakeOptional<OptionalProps<T>> & RequiredProps<T>>;
type DeepConvertObject<T> = ConvertObject<{ [P in keyof T]: DeepConvert<T[P]> }>;
type DeepConvert<T> = T extends object ? DeepConvertObject<T> : T;
type After = DeepConvert<Before>;
type SimpleAfter = DeepConvert<Simple>;
const fnBefore = (input: Before) => {
return input;
};
const fnAfter = (input: After) => {
return input;
};Expected behavior:
The tooltip should be shown without aliases.
Actual behavior:
The tooltip is shown with aliases.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
Experience EnhancementNoncontroversial enhancementsNoncontroversial enhancementsSuggestionAn idea for TypeScriptAn idea for TypeScript
Activity
yortus commentedon May 10, 2018
I'm seeing this too with a fairly similar recursive type. My use cases have only 1-2 levels of recursion. The compiler infers the recursive types correctly, but intellisense shows aliases after a single level of recursion.
I'm not sure the OP's quote from #22011 applies, since it's not clear that is talking about intellisense rather than the actual type within the compiler.
Regardless, it would be helpful when working in VSCode (or other IDEs) if intellisense didn't abbreviate the type quite so soon.
An example from my case:
If I inline
Flatten<T>into the other recursive types, then intellisense aliases another one of the mutually-recursive types instead.lstkz commentedon Jan 9, 2019
Troy Gerwien (@yortus)
Any update? Here is my library that has above issue https://github.com/BetterCallSky/veni.
It works fine in autocomplete after pressing
..But it's not user-friendly when you hover on the variable.
TypeScript Version: 3.2.2
lchimaru commentedon May 7, 2019
SANJAY KUMAR YADAV (@BetterCallSKY) have you tried just simply flattening the type? Look at this:
I know, it's showing following error
but surprisingly it works as it should. Take a look on hovering effect:

RyanCavanaugh commentedon Aug 22, 2019
If anyone can figure out a type alias printing strategy that doesn't regress current scenarios or generate stack overflows, we'd be happy to get a PR. The current rules are our best effort at heuristics that result in overall good results.
lstkz commentedon Oct 31, 2019
Łukasz Gosławski (@lchimaru)
wow, your fix works!
Ryan Cavanaugh (@RyanCavanaugh)
it seems like we must add guards
T extends objectand aliasing works for many nested levels.akutruff commentedon Feb 9, 2021
Ryan Cavanaugh (@RyanCavanaugh)
As per your request for suggestions on heuristic improvements, would the TS team be open to user supplied compiler hints for when the user is dealing with complex recursive conditional types?
The work around for this issue is the same for #28508 (comment) which is to flatten the hierarchy with another recursive conditional. That actually put the user at even more risk of hitting #34933. (I'm running typescript@next and keep hitting the infinite recursion problem.)
Similar to the the example here: #28508 (comment), and above, my code follows the same pattern:
Then we use the template on our actual type schemas. The following is the pattern used by many if not all the TypeScript validation libs, like io-ts, zod, etc. All of our application types are defined in this inverted way.
But, this destroys intellisense as relations emerge. (The
Address[owner]property is unreadable) So we use this work aroundProposal?
As the hierarchies start to grow deeper, the problem gets worse while the domain types like Person, and Address are natural caching points for the compiler to heuristically prune the search space. These domain objects are also where we want intellisense to simplify generics. The compiler doesn't know that Person and Address are where it can cache.
Instead, what if we give the compiler a hint to know when to fully compute and simplify a type, and then treat that type like an explicit type declaration from now on? From then on, the compiler and IDE treats Person and Address like as if the user had entered them as text in a .ts file. Intellisense would also ignore the underlying templates that defined the template driven domain type, and always show the most simplified form with recursion fully computed.
Some options:
Option 1: A new intrinsic. Pick a good name, I suck at naming.
Option 2: Whenever a type alias derives from an interface, then the type is fully inferred, and the compiler fully resolves the type as described above. Since I believe an interface can only extend a type that can be fully computed, then maybe this a natural spot. However, this is very verbose, and makes a weird empty interface in the code without clear purpose. Also, if the compiler ever does relax this requirement, then it breaks.