I interpret this as looking for a type function we can call TupMinMax<T, Min, Max> which resolves to a tuple type where each element is of type T, and whose length must be between Min and Max inclusive. We can represent this as a single tuple type with optional elements at every index greater than Min and less than or equal to Max. (Unless you turn on the --exactOptionalPropertyTypes compiler option, this will allow undefined values for these optional properties also, but I'm going to assume that's not a big deal). So you want, for example, TupMinMax<number, 1, 3> to be [number, number?, number?].
Here's one approach:
type TupMinMax<
T, Min extends number, Max extends number,
A extends (T | undefined)[] = [], O extends boolean = false
> = O extends false ? (
Min extends A['length'] ? TupMinMax<T, Min, Max, A, true> :
TupMinMax<T, Min, Max, [...A, T], false>
) : Max extends A['length'] ? A :
TupMinMax<T, Min, Max, [...A, T?], false>;
This is a tail-recursive conditional type, where TupMinMax<T, Min, Max> has some extra parameters A and O to act as accumulators to store intermediate state. Specifically, A will store the tuple result so far, while O will be either true or false representing whether we have entered into the optional part of the tuple. It starts out false and becomes true later.
The first conditional check is O extends false ? (...) : (...). If O is false then we haven't yet reached the minimum length and the elements should be required. Then we check Min extends A['length'] to see if the accumulator has reached the minimum length yet. If so, then we immediately switch O to true with the same A accumulator. If not, then we append a required T element to the end of A. If O is not false then it's true and we then check Max extends A['length'] to see if the accumulator has reached the maximum length yet. If so then we are done and evaluate to A. If not, then we append an optional T element to the end of A.
Let's test it out:
type OneTwoOrThreeNumbers = TupMinMax<number, 1, 3>;
// type OneTwoOrThreeNumbers = [number, number?, number?]
let nums: OneTwoOrThreeNumbers;
nums = []; // error
nums = [1]; // okay
nums = [1, 2]; // okay
nums = [1, 2, 3]; // okay
nums = [1, 2, 3, 4]; // error
nums = [1, "two", 3]; // error
type BetweenTenAndThirtyStrings = TupMinMax<string, 10, 30>;
/* type BetweenTenAndThirtyStrings = [string, string, string, string,
string, string, string, string, string, string, string?, string?,
string?, string?, ... 15 more ...?, string?] */
let strs: BetweenTenAndThirtyStrings;
strs = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14"]
Looks good. The tail recursion means that the compiler should be able to handle a recursion depth of ~1,000 levels, so if your tuple length range is even as large as several hundred it should compile okay.
Note that such recursive types can be fragile and prone to nasty edge cases. If you like to torture yourself and your compiler you try passing something other than non-negative whole numbers as Min and Max, or pass in a Min which is greater than Max. The recursion base case will never be reached and the compiler will, if you're lucky, complain about recursion depth; and if you're not lucky, it will consume lots of CPU and make your computer hot:
// 🔥💻🔥 followed by
// Type instantiation is excessively deep and possibly infinite.
// type Oops = TupMinMax<any, 10, 1>;
// type Oops2 = TupMinMax<any, 3.5 4>;
So be careful.
Playground link to code
[T,T,T] | [T,T,T,T] | [T,T,T,T,T]T been your enum.