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]]>;