Home > Enterprise >  Type definition for array with minimun and maximum length in Typescript
Type definition for array with minimun and maximum length in Typescript

Time:06-07

How can I create a type definition for array with minimun and maximum length, as in:

var my_arr:my_type<type, {start: 1, end: 3}>;

my_arr = [1] - pass
my_arr = [1,2] - pass
my_arr = [1,2,3] - pass
my_arr = [1,2,3,4] - fail
my_arr = [1, "string"] - fail

I.E. provide the type of the elements in the array and a start index and end index of length such that if the length is less than the start index it fails, or if the length is bigger than the end index it fails.

I've found this solution for a fixed length array online:

type SingleLengthArray<
  T,
  N extends number,
  R extends readonly T[] = []
> = R["length"] extends N ? R : SingleLengthArray<T, N, readonly [T, ...R]>;

And I was able to create a type that works if the length of the array is either equal to the start index or end index but nothing inbetween:

type MultipleLengthArray<
  T,
  N extends { start: number; end: number },
  R extends readonly T[] = []
> = SingleLengthArray<T, N["start"], R> | SingleLengthArray<T, N["end"], R>;

What I think would work if it's possible is to create an array of numbers from the start index up to the end index(i.e. start index = 1, end index = 5 => array would be [1,2,3,4,5]) and iterate over the array and like

type MultipleLengthArray<T, N extends {start: number, end:number}, R extends readonly T[] = []> = for each value n in array => SingleLengthArray<T, n1, R> | SingleLengthArray<T, n2, R> | ... | SingleLengthArray<T, n_last, R>

Please let me know if there is a way to do this, thank you.

CodePudding user response:

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:

//            
  • Related