13

How do you create a Typescript string type, which includes values from a union type AND is separated by commas?

I think this doesn't exist, but I'm asking anyway...

type AcceptableVal = 'cool' | 'wow' | 'biz' | 'sweet' | 'yowza' | 'fantastic';
type Amaze = // how do you define this?;

const dabes: Amaze = 'cool,wow';
const fancy: Amaze = 'biz,wow,fantastic';
const hachiMachi: Amaze = 'yowza,biz,sweet';
Ari
  • 1,595
  • 12
  • 20

3 Answers3

13

Typescript 4.3.4 Update

You can now use string template literal types to define a pattern like:

type S = 'cool' | 'wow' | 'biz' | 'sweet' | 'yowza' | 'fantastic';
type Test = `${S},${S}`

What you still can't do is make this infinitely extensible, like an array. To make this work typescript generates every single combination as a union. But for small lists like this it can work.

For example:

type S = 'cool' | 'wow' | 'biz' | 'sweet' | 'yowza' | 'fantastic';
type Amaze =
    | `${S}`
    | `${S},${S}`
    | `${S},${S},${S}`
    | `${S},${S},${S},${S}`
    | `${S},${S},${S},${S},${S}`
    | `${S},${S},${S},${S},${S},${S}`

If you hover over Amaze, you will see the type reported as:

type Amaze = S | "cool,cool" | "cool,wow" | "cool,biz" | "cool,sweet" 
| "cool,yowza" | "cool,fantastic" | "wow,cool" | "wow,wow"
| "wow,biz" | "wow,sweet" | "wow,yowza" | "wow,fantastic"
| ... 55967 more ...| "fantastic,fantastic,fantastic,fantastic,fantastic,fantastic"

See typescript playground

Notice that ... 55967 more .... Amaze is now a union with over fifty five thousand possible values. This may affect performance in your IDE at this point. And if you add the version that takes 7 strings you'll get a type error:

Expression produces a union type that is too complex to represent.(2590)

Eventually typescript cuts you off for performance sake. But again, for small lists, this may be viable.


Original answer: Typescript 3.7

You can't.

Typescript can type strings which can have any content, or can it can type exact strings like "cool" or "wow". But typescript will never know about if a string contains certain characters.

The only way to make this work would be to store these as an array instead:

type AmazeArray = AcceptableVal[];
const dabes: AmazeArray = ['cool', 'wow'];
Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • Yeah, that's what I figured. I want to type a function argument that's passed to a query param... Thank you for confirming. – Ari Dec 24 '19 at 19:12
8

with new template string feature of ts4.1 you can do this using following utils

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
    ? I
    : never;
type UnionToOvlds<U> = UnionToIntersection<U extends any ? (f: U) => void : never>;

type PopUnion<U> = UnionToOvlds<U> extends (a: infer A) => void ? A : never;

type UnionConcat<U extends string, Sep extends string> = PopUnion<U> extends infer SELF
    ? SELF extends string
        ? Exclude<U, SELF> extends never
            ? SELF
            :
                  | `${UnionConcat<Exclude<U, SELF>, Sep>}${Sep}${SELF}`
                  | UnionConcat<Exclude<U, SELF>, Sep>
                  | SELF
        : never
    : never;

example

type Keys = "a" | "b" | "c";
type Test = UnionConcat<Keys, ",">
// Test = "a" | "b" | "c" | "a,b" | "a,c" | "b,c" | "a,b,c"


type Test2 = UnionConcat<Keys, "-">
// Test2 = "a" | "b" | "c" | "a-b" | "a-c" | "b-c" | "a-b-c"

mh-alahdadian
  • 353
  • 5
  • 21
  • 1
    Could you show how this is used with the example from the OP please? – elwyn Apr 14 '21 at 04:11
  • This definitely looks promising but `UnionConcat<'red' | 'blue' | 'orange', 'mouse' | 'dog'>` is equal to `"red" | "blue" | "orange" | "red,mouse,blue" | "red,dog,blue" | "red,mouse,orange" | "red,dog,orange" | "blue,mouse,orange" | "blue,dog,orange" | "red,mouse,blue,mouse,orange" | "red,mouse,blue,dog,orange" | "red,dog,blue,mouse,orange" | "red,dog,blue,dog,orange"` and I really have no idea what that's supposed to mean! – Simon_Weaver Jun 30 '21 at 02:10
  • 1
    @Simon_Weaver You can appropriate this to a comma separated string list, for example: `type StringList = 'foo' | 'bar' | 'baz'` and passing it to `UnionConcat` eg: `UnionConcat` would return a unique commas separated list with all the values, eg: `'"foo" | "bar" | "baz" | "foo,bar" | "foo,baz" | "bar,baz" | "foo,bar,baz"`. It's actually rather elegant. Thanks @MHA15 for this. – User_coder Jul 02 '21 at 11:39
  • @User_coder yes it's definitely cool - I just wish he'd added an example or explanation. I mean if he was in a hurry I'd rather him just post this than not post it, but I didn't realize what 'Sep' meant (yes it's obvious with your example). – Simon_Weaver Jul 02 '21 at 18:53
  • 1
    Nice thing about this is you can actually define a type `type FooBarBazCombinations = UnionConcat` and then you get autocomplete when you type `const combination: FooBarBazCombinations = "..."`. Unfortunately you can only enter combinations in the order they are originally defined but that's probably a good thing! – Simon_Weaver Jul 02 '21 at 18:54
  • @Simon_Weaver 100% agree, should we add an additional answer to this question and include more information? – User_coder Jul 02 '21 at 20:19
  • @User_coder I think it's fair to add an example or two to the end of this answer for usage. Unless you have a separate version that allows keys in different orders. I attempted to do that before I found this answer and didn't quite crack it! I tried to 'exclude' the values you'd used so far but I didn't manage to complete it. – Simon_Weaver Jul 03 '21 at 03:58
  • thanks to your responses, just added some examples for you – mh-alahdadian Jul 03 '21 at 15:42
  • 1
    @MHA15 Is there a way to update this to include `"b-a" | "b-c" | etc`? – roydukkey Aug 13 '21 at 23:33
  • That's wicked! We can remove the lines `| UnionConcat, Sep>` and `| SELF` so that we rule out `"a" | "b" | "c"` and return only concatenated unions, as in `Test1 = "a,b" | "a,c" | "b,c" | "a,b,c"`. Genius! Thank you. – Moa Jul 26 '22 at 05:33
1

You can sort of do this with template literal types.

type GoodDog = 'ruger' | 'kim' | 'abbie' | 'bo' | 'jasper';

type DogList = `${ GoodDog }` | 
               `${ GoodDog },${ GoodDog }` | 
               `${ GoodDog },${ GoodDog },${ GoodDog }`;

const favoriteDogs1: DogList = 'kim,abbie,ruger'

This would support dog lists up to 3, and allow duplicates. You could add more as needed.

Unfortunately if you try to make this recursive it won't allow it. I think theoretically it may be possible to make a type that would be recursive with a 'tail condition' but I couldn't immediately figure it out.

A better way may be to use as const on an array (NOT a type).

 const Colors = ['red', 'blue', 'green', 'orange'] as const;

This is a runtime variable array, so you can display it in your UI - add it to a dropdown etc.

Then you need to extract the 'type' of this constant array (which is only possible because we made it const).

type ExtractArrayType<T> = T extends ReadonlyArray<infer U> ? U : never;

type ColorType = ExtractArrayType<typeof Colors>;

This will give us:

ColorType = 'red' | 'blue' | 'green' | 'orange;

Note that if we hadn't defined the original array with as const then ColorType would just be string.

We can now use this type and create an array;

const colorList: ColorType[] = ['red', 'blue'];

This approach is more useful for most runtime cases, although occasionally it would be nice to define something like red,blue and have that type checked.

Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689