0

Let's say I have a union type:

type UnionStr = 'foo' | 'bar' | 'baz';

I wonder if there is a type that infers the result as following:

type Join<T, Separator> = {...}

type Test = Join<UnionStr, ','> // -> foo,bar,baz
tmhao2005
  • 14,776
  • 2
  • 37
  • 44

1 Answers1

1

You can get close, but there's a problem: unions don't really have an order you can rely on. The compiler is fully within its rights to rearrange if it wants to. There's also some other weird cases with unions as well. I recommend looking at this answer by Jcalz about how to turn a union into a tuple for more details and pitfalls: https://stackoverflow.com/a/55128956/3794812

If we can change your requirements so the starting point is a tuple (eg, ['foo', 'bar', 'baz']) we'll be able to do it with no problem. But if you must start with a union, then we have to do a hacky and unreliable conversion from union to tuple. Which you shouldn't do. Seriously, see Jcalz's answer for all the reasons why you shouldn't do it.


Step 1: Utility to split a tuple into its Head and Tail

The plan is to take the tuple of strings and recursively join them together. To do that, i need to be able to break off the head ("foo", for the first step) and the tail (["bar", "baz"]). The head is easy, that's just T[0], where T is the tuple. For the tail, we have this utility type:

type Tail<T extends string[]> = T extends [string, ...infer TailType] ? TailType : never;

Step 2: Recursively do the joining

The following type takes in a string (H for "head"), an array of strings (T for "tail"), and a string for the separator. If the array has exactly one string in it, then the result is just to join them together with the separator, as in ${H}${Separator}${T[0]}. If there's more than one string, we need to recursively repeat the process, joining the head with whatever we get by joining the tail together

type JoinRecursive<H extends string, T extends string[], Separator extends string> = 
  Tail<T> extends [string, ...string[]] 
    ? `${H}${Separator}${JoinRecursive<T[0], Tail<T>>}` 
    : `${H}${Separator}${T[0]}`

Step 3: Provide an entry point

The recursive generic expects it to already be split into a head and a tail, so for convenience, i'll make a Join type that can take in the string tuple directly, and then will do that first split:

type Join<T extends string[], Separator extends string> = 
  Tail<T> extends string[] 
    ? JoinRecursive<T[0], Tail<T>, Separator> 
    : T[0];

Putting it together:

And here's how it then gets used:

type UnionTuple = ['foo', 'bar', 'baz'];
// If you must start from a union, the best that can be done is
//   Jcalz's TuplifyUnion hack. But don't do this.
// type UnionStr = 'foo' | 'bar' | 'baz';
// type UnionTuple = TuplifyUnion<UnionStr>;

type Test = Join<UnionTuple, ','> // 'foo,bar,baz'
type Test2 = Join<UnionTuple, '*'> // 'foo*bar*baz'

Playground link

Nicholas Tower
  • 72,740
  • 7
  • 86
  • 98
  • Thanks for the answer. I know that’s feasible for tuple type and only curious if there’s a easy way for union type. However it looks too complicated to make it happens which is unlikely cpu friendly. Anyway thanks for all your help! – tmhao2005 Dec 15 '22 at 06:05
  • You can generate the union type from the tuple by adding `as const` to it and then `type MyUnion = typeof tuple[number]`. – Valentin Dec 15 '22 at 06:23