Home > Mobile >  How to restrict an array to have every member of an enum in TypeScript
How to restrict an array to have every member of an enum in TypeScript

Time:05-20

enum AllowedFruits {
  Apple = 'APPLE',
  Banana = 'BANANA',
  Pear = 'PEAR'
}

const allowedFruits: AllowedFruits[] = [
  AllowedFruits.Apple, AllowedFruits.Banana, AllowedFruits.Pear
]

What I want to achieve is restricting an array to have every field of specific enum. I expect allowedFruits shows type error by adding or removing field of AllowedFruits.

Is there any way to achieve it?

If there are any articles or documents that I can refer to please let me know.

CodePudding user response:

Option 1

We can solve this by creating a type containing all possible combinations of AllowedFruits.

type AllCombinations<T extends string> = [T] extends [never] 
  ? [] 
  : {
      [K in T]: [K, ...AllCombinations<Exclude<T, K>>]
    }[T]

type AllFruitCombinations = AllCombinations<AllowedFruits>

This may result in bad performance if you have a lot of elements inside the enum because every single combination needs to be calculated first.

Let's see if this works:

/* Error */
const t1: AllFruitCombinations = []
const t2: AllFruitCombinations = [AllowedFruits.Apple] 
const t3: AllFruitCombinations = [AllowedFruits.Apple, AllowedFruits.Banana]
const t4: AllFruitCombinations = [AllowedFruits.Apple, AllowedFruits.Banana, AllowedFruits.Pear, AllowedFruits.Pear]

/* OK */
const t5: AllFruitCombinations = [AllowedFruits.Apple, AllowedFruits.Banana, AllowedFruits.Pear]

Playground

Option 2

It is also possible to solve this by passing allowedFruits to a function with a generic type.

We can create a generic helper type ExhaustiveFruits which checks if all enum values are present in the array.

type ExhaustiveFruits<
  O extends AllowedFruits[],
  T extends AllowedFruits[] = O,
  P extends string = `${AllowedFruits}`
> = [P] extends [never]
  ? O
  : T extends [`${infer L}`]
    ? [P] extends [L]
      ? O
      : never
    : T extends [`${infer L}`, ...infer R] 
      ? R extends AllowedFruits[]
        ? ExhaustiveFruits<O, R, Exclude<P, L>>
        : never
      : never

The logic of ExhaustiveFruits is quite simple: It is a recursive type where we start with a union of all enum values as P and the tuple of AllowedFruits as T.

For each element of T, the string value of the element is inferred with '${infer L}'. Afterwards this value is removed from the P union with Exclude<P, L>.

Every iteration there is a check if P is empty with [P] extends [never] or if the last element of T is the last element of P with [P] extends [L]. If this is the case, the original tuple O can be returned. If T is empty but P has still AllowedFruits in its union, never is returned.

The type can be used in a generic function createAllowedFruitsArray like this:

function createAllowedFruitsArray<
  T extends AllowedFruits[]
>(arr: [...ExhaustiveFruits<T>]) : T {
  return arr
}

Some checks to see if this is working:

createAllowedFruitsArray(
  []                                                              // Error
)                                                                
createAllowedFruitsArray(
  [AllowedFruits.Apple]                                           // Error
)                                             
createAllowedFruitsArray(
  [AllowedFruits.Apple, AllowedFruits.Banana]                     // Error
)                       
createAllowedFruitsArray(
  [AllowedFruits.Apple, AllowedFruits.Banana, AllowedFruits.Pear] // OK
) 

Right now it would also be possible to use the same enum value multiple times, as long as all are used.

createAllowedFruitsArray(
  [AllowedFruits.Apple, 
   AllowedFruits.Banana, 
   AllowedFruits.Pear,
   AllowedFruits.Pear] // Also ok, even though Pear is twice in the array 
) 

But with a slight modification, we can also change this:

type ExhaustiveFruits<
  O extends AllowedFruits[],
  T extends AllowedFruits[] = O,
  P extends string = `${AllowedFruits}`
> = [P] extends [never]
  ? O["length"] extends 0
    ? O
    : never
  : T extends [`${infer L}`]
    ? [P] extends [L]
      ? O
      : never
    : T extends [`${infer L}`, ...infer R] 
      ? R extends AllowedFruits[]
        ? [L] extends [P] 
          ? ExhaustiveFruits<O, R, Exclude<P, L>>
          : never
        : never
      : never

Playground

  • Related