Option 1
We can solve this by creating a type containing all possible combinations of AllowedFruits.
type AllPermutations<T extends string | number> = [T] extends [never]
? []
: {
[K in T]: [K, ...AllPermutations<Exclude<T, K>>]
}[T]
type AllFruitPermutations = AllPermutations<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 */
/* Error */
const t1: AllFruitPermutations = []
const t2: AllFruitPermutations = [AllowedFruits.Apple]
const t3: AllFruitPermutations = [AllowedFruits.Apple, AllowedFruits.Banana]
const t4: AllFruitPermutations = [AllowedFruits.Apple, AllowedFruits.Banana, AllowedFruits.Pear, AllowedFruits.Pear]
/* OK */
const t5: AllFruitPermutations = [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 | 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