4

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.

8
  • 1
    Does this answer your question? TypeScript array with minimum length Commented Jun 6, 2022 at 15:56
  • Minimum I get, but why would you ever need to specify the maximum length of an array via types? Unless we're talking about using an array as a tuple, in which case it has an exact length, or one of several kinds of tuples, in which case it's a union of fixed-length tuple types, I don't get why you'd need to strongly type the maximum length. Not to mention that arrays are mutable and growable so the typechecker could definitely lull you into a false sense of security with that. Commented Jun 6, 2022 at 16:00
  • @JaredSmith I need an array that has as parameters this enum {V, A, S} and it can have either a length of 3, 4 or 5, and all the possible orders of those enums(i.e. [S,S,S] or [V,A,S] or [S,A,V] or [S,S,S,V,A] etc.. If you know of a better method please let me know. And for the growable problem, I only need to replace the original array with another one, i.e. [V,A,S] replaced by [S,S,S,V,A], I'm never going to use methods such as push or other ones that modify the array's length directly(because I don't need to) Commented Jun 6, 2022 at 16:08
  • 3, 4, or 5, is just [T,T,T] | [T,T,T,T] | [T,T,T,T,T] T been your enum. Commented Jun 6, 2022 at 16:17
  • In addition to what Keith said, I understand the criteria, but who cares if there are extra elements (of the correct type) in the array? Slice the part you care about and call it a day. Way less error-prone than trying to make the type system bend over backwards. Commented Jun 6, 2022 at 16:19

2 Answers 2

4

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

Sign up to request clarification or add additional context in comments.

2 Comments

Great answer with great explanation, thank you so much. Just one thing, maybe I understood incorrectly but on the line "If so, then we immediately switch O to false", should this not be true instead corresponding to this line Min extends A['length'] ? TupMinMax<T, Min, Max, A, true> ?
Yeah, whoops, typo.
1

You may be able to leverage a Tuple with Optional Properties. Tuples have a specified length (allowing for the optionality of its properties).

enum E { V, A, S }

type TupleOfE = [ E, E, E, E?, E? ]

const e: TupleOfE = [ E.V, E.A, E.S, E.S, E.S ]

console.log(e)
console.log(e.filter(e => e === E.S))

1 Comment

This solves my problem, partially. The example I gave in the comments was not the only case scenario, I will have bigger sized arrays with the similar logic, so is there a cleaner way to create TupleOfE? I'd rather not type out 80 E's hahaha.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.