0

I want to have an array or set of values that is guaranteed to have all values of a string union. I know that I can do the reverse (TypeScript String Union to String Array) but in my case, the types are coming from a library.

So, imagine you have a union of strings given

type A = 'foo' | 'bar';

Now you want to create an array or set that has all values of the union of strings (not more or less).

  1. const a = ['foo', 'bar']; should pass because it has all values

  2. const a = ['foo']; should fail because 'bar' is missing

  3. const a = ['foo', 'bar', 'baz']; should fail because baz is not a valid A. This one is easy: const a: A[] = ['foo', 'bar', 'baz'];.

Since type information is lost at runtime, I know that I have to list the keys manually but I am okay with that.

I am aware of a trick, which works for objects but I would like to have something similar to arrays or sets.

type Flag<S extends string> = {[K in S]: 1};
const a = Flag<A> = {
  foo: 1,
  bar: 1
}

EDIT: here is a related issue on Typescript: https://github.com/microsoft/TypeScript/issues/13298

dominik
  • 5,745
  • 6
  • 34
  • 45
  • 1
    You are looking for `UnionToTuple` you will probably find it. Don't use it! It's unsupported and broken in interesting ways since union order MAY BE INCONSISTENT BETWEEN BUILDS. So short story stick with the object workaround. – Titian Cernicova-Dragomir Jul 09 '19 at 21:39
  • ^ UnionToTuple is unreliable because unions are ordered by an internal id system behind the type system meaning that it becomes very brittle to change in your application because the ordering could essentially change underneath you meaning `string | number` could randomly become `[number, string]` at any given point in time. However..... You can use UnionToTuple Permutations which may solve your problem... – Shanon Jackson Jul 09 '19 at 21:59

2 Answers2

1
type Overwrite<T, S extends any> = { [P in keyof T]: S[P] };
type TupleUnshift<T extends any[], X> = T extends any ? ((x: X, ...t: T) => void) extends (...t: infer R) => void ? R : never : never;
type TuplePush<T extends any[], X> = T extends any ? Overwrite<TupleUnshift<T, any>, T & { [x: string]: X }> : never;

type UnionToTuple<U> = UnionToTupleRecursively<[], U>;

type UnionToTupleRecursively<T extends any[], U> = {
    1: T;
    0: UnionToTupleRecursively_<T, U, U>;
}[[U] extends [never] ? 1 : 0]

type UnionToTupleRecursively_<T extends any[], U, S> =
    S extends any ? UnionToTupleRecursively<TupleUnshift<T, S> | TuplePush<T, S>, Exclude<U, S>> : never;

This code generates permutations of a union as a union of tuples. You can use it solve your problem like so...

type PermutationsOfA = UnionToTuple<A>; // ["foo", "bar"] | ["bar", "foo"]

const test: PermutationsOfA = ["foo", "bar"] // pass
const test1: PermutationsOfA = ["bar", "foo"] // pass
const test2: PermutationsOfA = ["bar", "foo", "baz"] // fail
const test3: PermutationsOfA = ["foo"] // fail

Permutations of UnionToTuple is resilient to changes that normally break non-permutations of UnionToTuple such as Typescripts internal ID system changing the ordering of unions.

Let me know if this helps

Shanon Jackson
  • 5,873
  • 1
  • 19
  • 39
  • Wow, that works. However, I have types with tens of possible values and finding all permutations is a bit expensive. I think I'll stick with my simple solution even though it's less pretty. – dominik Jul 09 '19 at 22:41
  • No problem that's when the recursive solutions start to die – Shanon Jackson Jul 09 '19 at 23:32
  • @ShanonJackson Yeah, generating all permutations is an idea, I just fear for anything more that a trivial example this will kill the compiler. I mean for a union of 10 elements, you have 10! permutations which is 3628800. And 10 does not seem like a large number of elements in a union. My previous comment stands don't use. Or at least use VERY VERY sparingly and when your compiler grinds to a halt consider this code as the problem it probably will be. – Titian Cernicova-Dragomir Jul 10 '19 at 07:09
  • Yeah absolutely you reach 10 you're compilers finished UnionToTuple is a hack agree 100% – Shanon Jackson Jul 10 '19 at 09:06
  • I have 22 in one of the examples I looked at and that's not even the biggest one. I guess Typescript needs a better solution to this problem. – dominik Jul 11 '19 at 11:30
0

Here is a slightly shorter solution:

type AllPermutations<T extends string> = [T] extends [never] ? [] : {
  [K in T]: [
    K, ...(AllPermutations<Exclude<T, K>> extends infer U extends string[] 
      ? U 
      : never)
  ]
}[T]

Playground

Tobias S.
  • 21,159
  • 4
  • 27
  • 45
  • Not sure why but this pretty much brings VSCode to its knees. Could be a personal setup issue but just sharing my experience. – Adrian Adkison Oct 14 '22 at 20:28