4

I wonder whether it's possible to “split” union types into the specific subtypes in TypeScript. This is the code I tried to use, it should be obvious what I'm trying to achieve from the snippet:

type SplitType<T> =
T extends (infer A)|(infer B)
? Something<A, B>
: T;

In this example Something<A, B> could be [A, B], or a completely different type. This would mean that SplitType<string> would just output a string, but SplitType<number|string> would mean [number, string].

Is something like that possible in TypeScript? And if not, is there a feature that will allow this in the future (eg. variadic types)?

m93a
  • 8,866
  • 9
  • 40
  • 58
  • 1
    You can turn the union to an intersection.. https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type and conditional types do distribute over unions so you can turn a union into a union of arrays of each member for example.But your specific example of tuning the union to a tuple I don't think is possible, at least not for an arbitrary number of tuple members (we might get it to work for a specific number of members but I have a feling we'd end up with something like `[number, string] | [string, number]`) – Titian Cernicova-Dragomir Oct 22 '18 at 14:14

2 Answers2

9

With Matt McCutchen's answer, and the recursive conditional types since v4.1:

type UnionToParm<U> = U extends any ? (k: U) => void : never;
type UnionToSect<U> = UnionToParm<U> extends ((k: infer I) => void) ? I : never;
type ExtractParm<F> = F extends { (a: infer A): void } ? A : never;

type SpliceOne<Union> = Exclude<Union, ExtractOne<Union>>;
type ExtractOne<Union> = ExtractParm<UnionToSect<UnionToParm<Union>>>;

type ToTuple<Union> = ToTupleRec<Union, []>;
type ToTupleRec<Union, Rslt extends any[]> = 
    SpliceOne<Union> extends never ? [ExtractOne<Union>, ...Rslt]
    : ToTupleRec<SpliceOne<Union>, [ExtractOne<Union>, ...Rslt]>
;

type test = ToTuple<5 | 6 | "l">;
SE12938683
  • 196
  • 1
  • 7
  • Awesome, but can anyone explain me why a type which should return the inferred parameter type I actually returns an intersection of the original function types? `type t = ((k: 5) => void) & ((k: 6) => void) & ((k: "l") => void)`. First of all why is the result an intersection? Which typescript rule applies here? And second, I would have expected something like `5 | 6 | 'l'` to be returned. Looks like weird black magic to me. Can you tell me the rules which apply here and make it work? – fragsalat Feb 17 '23 at 11:58
  • @fragsalat Which type is the said "a type which..."? – SE12938683 Mar 04 '23 at 08:00
7

For a fixed maximum number of union members, we can extract the union members in a single implementation-defined order by generating an intersection of call signatures and then matching it against a type with multiple call signatures. This version only works with strictFunctionTypes enabled.

// https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

type UnionToFunctions<U> =
    U extends unknown ? (k: U) => void : never;

type IntersectionOfFunctionsToType<F> =
    F extends { (a: infer A): void; (b: infer B): void; (c: infer C): void; } ? [A, B, C] :
    F extends { (a: infer A): void; (b: infer B): void; } ? [A, B] :
    F extends { (a: infer A): void } ? [A] :
    never;

type SplitType<T> =
    IntersectionOfFunctionsToType<UnionToIntersection<UnionToFunctions<T>>>;

type Test1 = SplitType<number>;                    // [number]
type Test2 = SplitType<number | string>;           // [string, number]
type Test3 = SplitType<number | string | symbol>;  // [string, number, symbol]
Matt McCutchen
  • 28,856
  • 2
  • 68
  • 75