Reputation: 1672
I have the following union type:
type Test =
| {
type: 'Union'
items: Array<
| { type: 'Number' }
| { type: 'String' }
| { type: 'Union'; items: Array<{ type: 'Number' } | { type: 'Boolean' }> }
>
}
| { type: 'Date' }
And I would like the result to be:
type Result =
| { type: 'Number' }
| { type: 'String' }
| { type: 'Boolean' }
| { type: 'Date' }
The following doesn't work because of the circular reference limitation:
type Flatten<T> = T extends { type: 'Union'; items: unknown[] }
? Flatten<T['items'][number]>
: T
Is it possible to solve using an interface or another alternative?
Upvotes: 2
Views: 842
Reputation: 327964
As far as I know there is (as of TS3.5) no supported technique to do a recursive conditional type function in general. It is generally okay if the circularity involves pushing types down into nested properties, but pulling types out of nested properties is not supported. All the "clever" tricks I've tried (or seen tried) to fool the compiler into accepting such things have ended in sadness.
Generally what I do when I want a recursive type like this is to pick some reasonable depth of recursion at which to bail out. In this case, how deeply nested are you expecting the T
to typically be in Flatten<T>
... let's say six layers deep at maximum? Well, in that case, we can unroll the circular type into a series of non-circular types that hit a brick wall at the end:
type Flatten<T> = T extends { type: 'Union'; items: unknown[] } ? Flatten1<T['items'][number]> : T;
type Flatten1<T> = T extends { type: 'Union'; items: unknown[] } ? Flatten2<T['items'][number]> : T;
type Flatten2<T> = T extends { type: 'Union'; items: unknown[] } ? Flatten3<T['items'][number]> : T;
type Flatten3<T> = T extends { type: 'Union'; items: unknown[] } ? Flatten4<T['items'][number]> : T;
type Flatten4<T> = T extends { type: 'Union'; items: unknown[] } ? Flatten5<T['items'][number]> : T;
type Flatten5<T> = T extends { type: 'Union'; items: unknown[] } ? Flatten6<T['items'][number]> : T;
type Flatten6<T> = T extends { type: 'Union'; items: unknown[] } ? FlattenX<T['items'][number]> : T;
type FlattenX<T> = T; // uh oh 🧱🧱 bail out
That might be ugly, but it generally has manageable effects. Let's see how it does with your types:
type Result = Flatten<Test>;
// type Result = { type: "Date"; } | { type: "Number"; } |
// { type: "String"; } | { type: "Number"; } | { type: "Boolean"; }
Looks good to me. Okay, hope that helps. Good luck!
Upvotes: 2