Recursive conditional types are aliased
See original GitHub issueI 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.
Issue Analytics
- State:
- Created 6 years ago
- Reactions:2
- Comments:6 (2 by maintainers)
Top Results From Across the Web
Recursive Types alias in typescript - typing flatten function
The answer is that even if the recursion in types is allowed it's only in a subset of cases. Namely when using a...
Read more >Notes on TypeScript: Recursive Type Aliases and Immutability
Now that we learned about the recursive type aliases, let's create an immutable type that we can use to add more guarantees into...
Read more >Documentation - TypeScript 4.1
In TypeScript 4.1, conditional types can now immediately reference themselves within their branches, making it easier to write recursive type aliases.
Read more >Announcing TypeScript 4.1 Beta - Microsoft Developer Blogs
... Types; Key Remapping in Mapped Types; Recursive Conditional Types ... and the in-progress pull request to switch to type alias helpers.
Read more >Match Types - Scala 3 - EPFL
Match types can form part of recursive type definitions. ... Conditional types only reduce if both the scrutinee and pattern are ground, whereas...
Read more >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
@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 https://github.com/microsoft/TypeScript/issues/28508#issuecomment-775904459 which is to flatten the hierarchy with another recursive conditional. That actually put the user at even more risk of hitting https://github.com/microsoft/TypeScript/issues/34933#. (I’m running typescript@next and keep hitting the infinite recursion problem.)
Similar to the the example here: https://github.com/microsoft/TypeScript/issues/28508#issue-380418556, 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.
@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: