25

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
3
  • I'm not writing TypeScript at all and never heard about union types before. After having read this section ... typescriptlang.org/docs/handbook/… ... 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. Commented Feb 8, 2020 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. Commented Feb 8, 2020 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]; Commented Feb 9, 2020 at 7:02

2 Answers 2

33

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
Sign up to request clarification or add additional context in comments.

4 Comments

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?
Is it also possible with objects instead of strings? E.g. interface White {name: "white", hex: "#fff"} type Colors = White | Red
How could you make this work with template literals as well? e.g. if Colors contained blue_${intensity}? (non-working) playground here
arrayOfAll 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;
6

@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;

1 Comment

how do i omit existing values from type suggestion?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.