Reputation: 25986
In my answer to Can Typescript Interfaces express co-occurrence constraints for properties I produced following code:
type None<T> = {[K in keyof T]?: never}
type EitherOrBoth<T1, T2> = T1 & None<T2> | T2 & None<T1> | T1 & T2
type CombinationOf<T> = T extends [infer U1, infer U2] ? EitherOrBoth<U1, U2> :
T extends [infer U1, infer U2, infer U3] ? EitherOrBoth<U1, EitherOrBoth<U2, U3>> :
T extends [infer U1, infer U2, infer U3, infer U4] ? EitherOrBoth<U1, EitherOrBoth<U2, EitherOrBoth<U3, U4>>> :
never;
type Monolith = CombinationOf<[Data1, Data2, Data3]>
Currently, CombinationOf
handles tuple of type arguments, up to four elements.
While this can be trivially extended, I wonder if, with the advent of recursive conditional types, type CombinationOf
can be expressed in more elegant way.
Thus, my questions are:
Upvotes: 2
Views: 196
Reputation: 328362
In TypeScript 4.1 when recursive conditional types are introduced, you should be able to define CombinationOf<T>
like this:
type CombinationOf<T> =
T extends [infer U1, ...infer R] ? (
R extends [] ? never : EitherOrBoth<U1,
R extends [infer U2] ? U2 : CombinationOf<R>
>
) : never;
Here I've tried to stick as close as possible to your original definition without worrying about what the EitherOrBoth<>
type is supposed to really be doing. Note that because your original definition returns never
when T
is a one-element tuple, the definition here is a little more compilcated. (I'd sort of expect CombinationOf<[X]>
to just be X
. But 🤷♂️)
If T
has 0 or 1 elements, you get never
. If there are two elements, you get EitherOrBoth<U1, U2>
where U1
and U2
are the first two elements of T
. Otherwise, you get EitherOrBoth<U1, CombinationOf<R>>
where R
is the rest/tail of the tuple T
after U1
.
Yes, there is a depth limit and it's not very deep; something like 10 levels:
type TestDepth = CombinationOf<[1, 2, 3, 4, 5, 6, 7, 8, 9]> // works for me
type TestDepthBad = CombinationOf<[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]> // error, too deep
You can probably rewrite CombinationOf
to look more like your original version where it chops the tuple T
into groups of 3 or 4 instead of groups of 1 and get the depth limit to increase (maybe to 30 or 40... edit: playing around with this I can't seem to get it to go above 25 or so) at the expense of an even more complicated CombinationOf
. Not sure it's worth it, though.
Proving that the types are the same could either be done by just inspecting the definitions of this CombinationOf
and yours (which I'll call CombinationOfOrig
), or by building an inspector type function:
type IfEquals<T, U, Y = unknown, N = never> =
(<G>() => G extends T ? 1 : 2) extends
(<G>() => G extends U ? 1 : 2) ? Y : N;
type TestEqualCombinationOf<T> =
IfEquals<CombinationOfOrig<T>, CombinationOf<T>, "Same", "Oops">;
Here, IfEquals<T, U, Y, N>
will evaluate to Y
if the compiler sees T
and U
as identical types, and N
otherwise. See this comment in GitHub for more info about this type equality operator.
And TestEqualCombinationOf<T>
will evaluate to "Same"
if CombinationOfOrig<T>
is identical to CombinationOf<T>
, and "Oops"
otherwise. Here are the results of the tests:
type TestEmpty = TestEqualCombinationOf<[]> // Same
type TestSingleton = TestEqualCombinationOf<[Data1]> // Same
type TestPair = TestEqualCombinationOf<[Data1, Data2]>; // Same
type TestTriple = TestEqualCombinationOf<[Data1, Data2, Data3]>; // Same
interface Data4 { four: 4 }
type TestQuadruple = TestEqualCombinationOf<[Data1, Data2, Data3, Data4]>; // Same
interface Data5 { five: 5 }
type TestQuintuple = TestEqualCombinationOf<[Data1, Data2, Data3, Data4, Data5]>; // Oops
So experimentally, at least, the two definitions are the same for any tuple up to length 4. I was actually a bit worried about TestQuintuple
being "Oops"
until I remembered that the original definition only worked for tuples of length up to 4. So that "Oops"
is expected.
Upvotes: 3