21

Say I have a union that looks like this

type Colors = 'red' | 'blue' | 'pink'

Is it possible to type check an array against this union and make sure the array contains all of the types?

I.e.:

const colors: UnionToTuple<Colors> = ['red', 'blue']  // type error, missing 'pink'
const colors: UnionToTuple<Colors> = ['red', 'blue', 'pink']  // no error
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Samuel
  • 2,485
  • 5
  • 30
  • 40
  • I'm not writing TypeScript at all and never heard about union types before. After having read this section ... https://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types ... within the last 5 minutes I'm not quite sure whether I did not grasp the concept of union types ... `If we have a value that has a union type, we can only access members that are common to all types in the union.` What the example does show me are just pipe separated `string` values and not different types of color objects. I'm not even sure if this union definition of yours does make any sense. – Peter Seliger Feb 08 '20 at 22:49
  • 1
    @PeterSeliger It's possible, and makes sense to me. You can create an array type of every item in a union with syntax like `Colors[]`, and you can check an array against it with generics and a helper function. – CertainPerformance Feb 08 '20 at 23:15
  • If the array is defined at compile time, it is much easier to go the other way around: define an array, then infer the union type from it, so you type the values only once `const colors = ['red', 'blue', 'pink'] as const; type Colors = typeof colors[number];` – Aleksey L. Feb 09 '20 at 07:02

2 Answers2

25

Taking ideas from this answer, you can make a function that type-checks by using a type parameter of the passed array, T. Type it as T extends Colors[] to make sure every item of the array is in Colors, and also type it as [Colors] extends [T[number]] ? unknown : 'Invalid' to make sure every item of the Colors type is in the passed array:

type Colors = 'red' | 'blue' | 'pink';
const arrayOfAllColors = <T extends Colors[]>(
  array: T & ([Colors] extends [T[number]] ? unknown : 'Invalid')
) => array;

const missingColors = arrayOfAllColors(['red', 'blue']); // error
const goodColors = arrayOfAllColors(['red', 'blue', 'pink']); // compiles
const extraColors = arrayOfAllColors(['red', 'blue', 'pink', 'bad']); // error

More generically, wrap it in another function so you can pass and use a type parameter of the union type:

type Colors = 'red' | 'blue' | 'pink';
const arrayOfAll = <T>() => <U extends T[]>(
  array: U & ([T] extends [U[number]] ? unknown : 'Invalid')
) => array;
const arrayOfAllColors = arrayOfAll<Colors>();

const missingColors = arrayOfAllColors(['red', 'blue']); // error
const goodColors = arrayOfAllColors(['red', 'blue', 'pink']); // compiles
const extraColors = arrayOfAllColors(['red', 'blue', 'pink', 'bad']); // error
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • 1
    Amazing, thanks for your help - is it possible to generalise the Colors type in the function so that i can pass a union in when calling it? – Samuel Feb 08 '20 at 23:20
  • Is it also possible with objects instead of strings? E.g. interface White {name: "white", hex: "#fff"} type Colors = White | Red – Martin Jaskulla Jan 27 '21 at 12:29
  • How could you make this work with template literals as well? e.g. if Colors contained `blue_${intensity}`? (non-working) playground [here](https://www.typescriptlang.org/play?#code/C4TwDgpgBAKhDOwoF4oHIBmB7LaoB90AjAQwCc9CADUgLwH0ASAb0TIEsA7AcwF8qAsAChhGAK6cAxsHZZOUcmRIgA8hgCCAG00AeGAD4oACgCUUZsKhQyEYGLLzxUmXJ0BVKBAAewCJwAm8LAA2gC6hkaKygBcUB4AZMbBMKGePn6BUMFuwZxiALZEEGShqQD8UBIA1pxYAO7ysWgAkpwAbiSa7P5oJmYWQlZWNnYOCmRKIADcllC8M0K8wsJRqhraegjA+qZGwejYuAA0xORoJ2h09ACMeKEmU1AA9E9QdVhimv5QXVXQwAALdhBWpIYBYTwTLBkFYTZRqLS6OCIHYmPYHHDnU4UC5XW64kgMABMdwez1e70+31+-yBIKwYIhxTI0OEQA) – Chris Nov 24 '22 at 18:23
  • arrayOfAll does not reject empty arrays. Here is a fix: const arrayOfAll = () => ( array: U & ([T] extends [U[number]] ? unknown : 'Invalid') & { 0:T } ) => array; – Brian Takita Feb 11 '23 at 07:01
1

@CertainPerformance's solution does not reject empty arrays. Here is a fix:

const arrayOfAll = <T>() => <U extends T[]>(
  array: U & ([T] extends [U[number]] ? unknown : 'Invalid') & { 0:T }
) => array;
Brian Takita
  • 1,615
  • 1
  • 15
  • 20