如何限制数组在 TypeScript 中包含枚举的每个成员

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

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

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

我想要实现的是限制一个数组具有特定枚举的每个字段。 我希望 allowedFruits 通过添加或删除 AllowedFruits 的字段显示类型错误。

有什么办法可以实现吗?

如果有任何我可以参考的文章或文档,请告诉我。

选项 1

我们可以通过创建一个包含 AllowedFruits.

所有可能组合的类型来解决这个问题
type AllCombinations<T extends string | number> = [T] extends [never] 
  ? [] 
  : {
      [K in T]: [K, ...AllCombinations<Exclude<T, K>>]
    }[T]

type AllFruitCombinations = AllCombinations<AllowedFruits>

如果枚举中有很多元素,这可能会导致性能不佳,因为每个组合都需要先计算。

让我们看看这是否有效:

/* 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

选项 2

也可以通过将 allowedFruits 传递给具有通用类型的函数来解决此问题。

我们可以创建一个通用的辅助类型 ExhaustiveFruits 来检查数组中是否存在所有枚举值。

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

ExhaustiveFruits 的逻辑非常简单:它是一个递归类型,我们从所有枚举值的并集开始作为 PAllowedFruits 的元组作为 T.

对于 T 的每个元素,元素的 string 值由 '${infer L}' 推断。之后,此值从 PExclude<P, L> 的联合中删除。

每次迭代都会检查 P 是否为空 [P] extends [never] 或者 T 的最后一个元素是否是 P 的最后一个元素 [P] extends [L].如果是这种情况,可以返回原始元组 O。如果 T 为空但 P 的联合中仍有 AllowedFruits,则返回 never

类型可以在泛型函数中使用 createAllowedFruitsArray 像这样:

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

检查一下是否有效:

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

现在也可以多次使用相同的枚举值,只要全部使用即可。

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

但稍微修改一下,我们也可以这样改:

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

Playground