How to declare a Fixed length Array in TypeScript How to declare a Fixed length Array in TypeScript typescript typescript

How to declare a Fixed length Array in TypeScript


The javascript array has a constructor that accepts the length of the array:

let arr = new Array<number>(3);console.log(arr); // [undefined × 3]

However, this is just the initial size, there's no restriction on changing that:

arr.push(5);console.log(arr); // [undefined × 3, 5]

Typescript has tuple types which let you define an array with a specific length and types:

let arr: [number, number, number];arr = [1, 2, 3]; // okarr = [1, 2]; // Type '[number, number]' is not assignable to type '[number, number, number]'arr = [1, 2, "3"]; // Type '[number, number, string]' is not assignable to type '[number, number, number]'


The Tuple approach :

This solution provides a strict FixedLengthArray (ak.a. SealedArray) type signature based in Tuples.

Syntax example :

// Array containing 3 stringslet foo : FixedLengthArray<[string, string, string]> 

This is the safest approach, considering it prevents accessing indexes out of the boundaries.

Implementation :

type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' | 'unshift' | numbertype ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : nevertype FixedLengthArray<T extends any[]> =  Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>  & { [Symbol.iterator]: () => IterableIterator< ArrayItems<T> > }

Tests :

var myFixedLengthArray: FixedLengthArray< [string, string, string]>// Array declaration testsmyFixedLengthArray = [ 'a', 'b', 'c' ]  // ✅ OKmyFixedLengthArray = [ 'a', 'b', 123 ]  // ✅ TYPE ERRORmyFixedLengthArray = [ 'a' ]            // ✅ LENGTH ERRORmyFixedLengthArray = [ 'a', 'b' ]       // ✅ LENGTH ERROR// Index assignment tests myFixedLengthArray[1] = 'foo'           // ✅ OKmyFixedLengthArray[1000] = 'foo'        // ✅ INVALID INDEX ERROR// Methods that mutate array lengthmyFixedLengthArray.push('foo')          // ✅ MISSING METHOD ERRORmyFixedLengthArray.pop()                // ✅ MISSING METHOD ERROR// Direct length manipulationmyFixedLengthArray.length = 123         // ✅ READ-ONLY ERROR// Destructuringvar [ a ] = myFixedLengthArray          // ✅ OKvar [ a, b ] = myFixedLengthArray       // ✅ OKvar [ a, b, c ] = myFixedLengthArray    // ✅ OKvar [ a, b, c, d ] = myFixedLengthArray // ✅ INVALID INDEX ERROR

(*) This solution requires the noImplicitAny typescript configuration directive to be enabled in order to work (commonly recommended practice)


The Array(ish) approach :

This solution behaves as an augmentation of the Array type, accepting an additional second parameter(Array length). Is not as strict and safe as the Tuple based solution.

Syntax example :

let foo: FixedLengthArray<string, 3> 

Keep in mind that this approach will not prevent you from accessing an index out of the declared boundaries and set a value on it.

Implementation :

type ArrayLengthMutationKeys = 'splice' | 'push' | 'pop' | 'shift' |  'unshift'type FixedLengthArray<T, L extends number, TObj = [T, ...Array<T>]> =  Pick<TObj, Exclude<keyof TObj, ArrayLengthMutationKeys>>  & {    readonly length: L     [ I : number ] : T    [Symbol.iterator]: () => IterableIterator<T>     }

Tests :

var myFixedLengthArray: FixedLengthArray<string,3>// Array declaration testsmyFixedLengthArray = [ 'a', 'b', 'c' ]  // ✅ OKmyFixedLengthArray = [ 'a', 'b', 123 ]  // ✅ TYPE ERRORmyFixedLengthArray = [ 'a' ]            // ✅ LENGTH ERRORmyFixedLengthArray = [ 'a', 'b' ]       // ✅ LENGTH ERROR// Index assignment tests myFixedLengthArray[1] = 'foo'           // ✅ OKmyFixedLengthArray[1000] = 'foo'        // ❌ SHOULD FAIL// Methods that mutate array lengthmyFixedLengthArray.push('foo')          // ✅ MISSING METHOD ERRORmyFixedLengthArray.pop()                // ✅ MISSING METHOD ERROR// Direct length manipulationmyFixedLengthArray.length = 123         // ✅ READ-ONLY ERROR// Destructuringvar [ a ] = myFixedLengthArray          // ✅ OKvar [ a, b ] = myFixedLengthArray       // ✅ OKvar [ a, b, c ] = myFixedLengthArray    // ✅ OKvar [ a, b, c, d ] = myFixedLengthArray // ❌ SHOULD FAIL


Actually, You can achieve this with current typescript:

type Grow<T, A extends Array<T>> = ((x: T, ...xs: A) => void) extends ((...a: infer X) => void) ? X : never;type GrowToSize<T, A extends Array<T>, N extends number> = { 0: A, 1: GrowToSize<T, Grow<T, A>, N> }[A['length'] extends N ? 0 : 1];export type FixedArray<T, N extends number> = GrowToSize<T, [], N>;

Examples:

// OKconst fixedArr3: FixedArray<string, 3> = ['a', 'b', 'c'];// Error:// Type '[string, string, string]' is not assignable to type '[string, string]'.//   Types of property 'length' are incompatible.//     Type '3' is not assignable to type '2'.ts(2322)const fixedArr2: FixedArray<string, 2> = ['a', 'b', 'c'];// Error:// Property '3' is missing in type '[string, string, string]' but required in type // '[string, string, string, string]'.ts(2741)const fixedArr4: FixedArray<string, 4> = ['a', 'b', 'c'];

EDIT (after a long time)

This should handle bigger sizes (as basically it grows array exponentially until we get to closest power of two):

type Shift<A extends Array<any>> = ((...args: A) => void) extends ((...args: [A[0], ...infer R]) => void) ? R : never;type GrowExpRev<A extends Array<any>, N extends number, P extends Array<Array<any>>> = A['length'] extends N ? A : {  0: GrowExpRev<[...A, ...P[0]], N, P>,  1: GrowExpRev<A, N, Shift<P>>}[[...A, ...P[0]][N] extends undefined ? 0 : 1];type GrowExp<A extends Array<any>, N extends number, P extends Array<Array<any>>> = A['length'] extends N ? A : {  0: GrowExp<[...A, ...A], N, [A, ...P]>,  1: GrowExpRev<A, N, P>}[[...A, ...A][N] extends undefined ? 0 : 1];export type FixedSizeArray<T, N extends number> = N extends 0 ? [] : N extends 1 ? [T] : GrowExp<[T, T], N, [[T]]>;