How to declare a Fixed length Array in TypeScript

TypescriptTypesFixed Length-Array

Typescript Problem Overview


At the risk of demonstrating my lack of knowledge surrounding TypeScript types - I have the following question.

When you make a type declaration for an array like this...

position: Array<number>;

...it will let you make an array with arbitrary length. However, if you want an array containing numbers with a specific length i.e. 3 for x,y,z components can you make a type with for a fixed length array, something like this?

position: Array<3>

Any help or clarification appreciated!

Typescript Solutions


Solution 1 - 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]; // ok
arr = [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]'

Solution 2 - Typescript

The Tuple approach :

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

Syntax example :

// Array containing 3 strings
let 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' | number
type ArrayItems<T extends Array<any>> = T extends Array<infer TItems> ? TItems : never
type FixedLengthArray<T extends any[]> =
  Pick<T, Exclude<keyof T, ArrayLengthMutationKeys>>
  & { [Symbol.iterator]: () => IterableIterator< ArrayItems<T> > }

Tests :

var myFixedLengthArray: FixedLengthArray< [string, string, string]>

// Array declaration tests
myFixedLengthArray = [ 'a', 'b', 'c' ]  // ✅ OK
myFixedLengthArray = [ 'a', 'b', 123 ]  // ✅ TYPE ERROR
myFixedLengthArray = [ 'a' ]            // ✅ LENGTH ERROR
myFixedLengthArray = [ 'a', 'b' ]       // ✅ LENGTH ERROR

// Index assignment tests 
myFixedLengthArray[1] = 'foo'           // ✅ OK
myFixedLengthArray[1000] = 'foo'        // ✅ INVALID INDEX ERROR

// Methods that mutate array length
myFixedLengthArray.push('foo')          // ✅ MISSING METHOD ERROR
myFixedLengthArray.pop()                // ✅ MISSING METHOD ERROR

// Direct length manipulation
myFixedLengthArray.length = 123         // ✅ READ-ONLY ERROR

// Destructuring
var [ a ] = myFixedLengthArray          // ✅ OK
var [ a, b ] = myFixedLengthArray       // ✅ OK
var [ a, b, c ] = myFixedLengthArray    // ✅ OK
var [ 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 tests
myFixedLengthArray = [ 'a', 'b', 'c' ]  // ✅ OK
myFixedLengthArray = [ 'a', 'b', 123 ]  // ✅ TYPE ERROR
myFixedLengthArray = [ 'a' ]            // ✅ LENGTH ERROR
myFixedLengthArray = [ 'a', 'b' ]       // ✅ LENGTH ERROR

// Index assignment tests 
myFixedLengthArray[1] = 'foo'           // ✅ OK
myFixedLengthArray[1000] = 'foo'        // ❌ SHOULD FAIL

// Methods that mutate array length
myFixedLengthArray.push('foo')          // ✅ MISSING METHOD ERROR
myFixedLengthArray.pop()                // ✅ MISSING METHOD ERROR

// Direct length manipulation
myFixedLengthArray.length = 123         // ✅ READ-ONLY ERROR

// Destructuring
var [ a ] = myFixedLengthArray          // ✅ OK
var [ a, b ] = myFixedLengthArray       // ✅ OK
var [ a, b, c ] = myFixedLengthArray    // ✅ OK
var [ a, b, c, d ] = myFixedLengthArray // ❌ SHOULD FAIL

Solution 3 - Typescript

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:

// OK
const 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]]>;

Solution 4 - Typescript

A little late to the party but here is one way if you are using read-only arrays ([] as const) -

interface FixedLengthArray<L extends number, T> extends ArrayLike<T> {
  length: L
}

export const a: FixedLengthArray<2, string> = ['we', '432'] as const

Adding or removing strings in const a value results in this error -

Type 'readonly ["we", "432", "fd"]' is not assignable to type 'FixedLengthArray<2, string>'.
  Types of property 'length' are incompatible.
    Type '3' is not assignable to type '2'.ts(2322)

OR

Type 'readonly ["we"]' is not assignable to type 'FixedLengthArray<2, string>'.
  Types of property 'length' are incompatible.
    Type '1' is not assignable to type '2'.ts(2322)

respectively.

EDIT (05/13/2022): Relevant future TS feature - satisfies defined here

Solution 5 - Typescript

With typescript v4.6, here's a super short version based on Tomasz Gawel's answer

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

// usage
const x: Tuple<number,3> = [1,2,3];
x; // resolves as [number, number, number]
x[0]; // resolves as number

There are other approaches that imposes the value of the length property, but it's not very pretty

// TLDR, don't do this
type Tuple<T, N> = { length: N } & readonly T[];
const x : Tuple<number,3> = [1,2,3]

x; // resolves as { length: 3 } | number[], which is kinda messy
x[0]; // resolves as number | undefined, which is incorrect

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionhenryJackView Question on Stackoverflow
Solution 1 - TypescriptNitzan TomerView Answer on Stackoverflow
Solution 2 - TypescriptcolxiView Answer on Stackoverflow
Solution 3 - TypescriptTomasz GawelView Answer on Stackoverflow
Solution 4 - TypescriptSushruthView Answer on Stackoverflow
Solution 5 - TypescriptThomas VoView Answer on Stackoverflow