282

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, e.g. 3 for x, y, z components. Can you make a type with for a fixed length array, something like this?

position: Array<3>
0

8 Answers 8

368

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]'
Sign up to request clarification or add additional context in comments.

5 Comments

The tuple types checks the initial size only, so you can still push unlimited amount of "number" to your arr after it's initialized.
True, it's still javascript at runtime to "anything goes" at that point. At least the typescript transpiler will enforce this in the source code at least
In case I want large array sizes like, say, 50, is there a way to specify array size with a repeated type, like [number[50]], so that it wouldn't be necessary to write [number, number, ... ] 50 times?
Never mind, found a question regarding this. stackoverflow.com/questions/52489261/…
@VictorZamanian Just so you're aware, the idea to intersect {length: TLength} does not provide any typescript error should you exceed the typed TLength. I haven't yet found a size-enforced n-length type syntax.
115

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

6 Comments

Thanks! However, it is still possible to change the size of array without getting an error.
var myStringsArray: FixedLengthArray<string, 2> = [ "a", "b" ] // LENGTH ERROR seems like 2 should be 3 here?
I've updated the implementation with a stricter solution that prevents array length changes
@colxi Is it possible to have an implementation that allows for mapping from FixedLengthArray's to other FixedLengthArray's? An example of what I mean: const threeNumbers: FixedLengthArray<[number, number, number]> = [1, 2, 3]; const doubledThreeNumbers: FixedLengthArray<[number, number, number]> = threeNumbers.map((a: number): number => a * 2);
@AlexMalcolm i'm afraid map provides a generic array signature to its output. In your case most likely a number[] type
|
33

The original answer was written some time ago, with typescript version 3.x. Since then the typescript version went as far as 4.94, some limitation of typescript has been lifted. Also the answer was modified due to some issues pointed in comments.

Original Answer

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

At that time (typescript 3.x), with this approach it was possible to construct relatively small tuples of size up to 20 items. For bigger sizes it produced "Type instantiation is excessively deep and possibly infinite". This problem was raised by @Micha Schwab in the comment below. This made to think about more efficient approach to growing arrays which resulted in the Edit 1.

EDIT 1: Bigger sizes (or "exponential growth")

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

This approach allowed to handle bigger tuple sizes (up to 2^15), although with numbers above 2^13 it was noticable slow.

This approach had also a problem with handling tuples of any, never and undefined. These types satisfy the extends undefined ? condition (the condition used to test if the index is out of generated array), and so would keep the recursion going infinitely. This problem was reported by @Victor Zhou in his comment.

EDIT 2: Tuples of never, any, or undefined

The "exponential array growth" approach cannot handle tuples of any, never and undefined. This can be solved by first preparing the tuple of some "not controversial type" then rewriting the tuple with requested size to requested item type.

type MapItemType<T, I> = { [K in keyof T]: I };

export type FixedSizeArray<T, N extends number> = 
    N extends 0 ? [] : MapItemType<GrowExp<[0], N, []>, T>;

Examples:

var tupleOfAny: FixedSizeArray<any, 3>; // [any, any, any]
var tupleOfNever: FixedSizeArray<never, 3>; // [never, never, never]
var tupleOfUndef: FixedSizeArray<undefined, 2>; // [undefined, undefined]

In the meantime current typescript version become 4.94. It's time summarize and clean up the code.

EDIT 3: Typescript 4.94

The original FixedArray type may be now written as simple as:

type GrowToSize<T, N extends number, A extends T[]> = 
  A['length'] extends N ? A : GrowToSize<T, N, [...A, T]>;

export type FixedArray<T, N extends number> = GrowToSize<T, N, []>;

This can now handle sizes up to 999.

let tuple999: FixedArray<boolean, 999>; 
// let tuple999: [boolean, boolean, boolean, boolean, boolean, boolean, boolean,
// boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean,
// boolean, boolean, ... 980 more ..., boolean]

let tuple1000: FixedArray<boolean, 1000>;
// let tuple1000: any
// Error:
// Type instantiation is excessively deep and possibly infinite. ts(2589)

So we may add safe guard to return array of T if tuple size exceeds 999.

type GrowToSize<T, N extends number, A extends T[], L extends number = A['length']> = 
  L extends N ? A : L extends 999 ? T[] : GrowToSize<T, N, [...A, T]>;
export type FixedArray<T, N extends number> = GrowToSize<T, N, []>;

let tuple3: FixedArray<boolean, 3>; // [boolean, boolean, boolean]
let tuple1000: FixedArray<boolean, 1000>; // boolean[]

The "exponential array growth" approach can now handle up to 8192 (2^13) tuple size.

Above that size, it raises "Type produces a tuple type that is too large to represent. ts(2799)".

We can write it, including safe guard at size of 8192, as below:

type Shift<A extends Array<any>> = 
  ((...args: A) => void) extends ((...args: [A[0], ...infer R]) => void) ? R : never;

type GrowExpRev<A extends any[], N extends number, P extends any[][]> = 
  A['length'] extends N ? A : [...A, ...P[0]][N] extends undefined ? GrowExpRev<[...A, ...P[0]], N, P> : GrowExpRev<A, N, Shift<P>>;

type GrowExp<A extends any[], N extends number, P extends any[][], L extends number = A['length']> = 
  L extends N ? A : L extends 8192 ? any[] : [...A, ...A][N] extends undefined ? GrowExp<[...A, ...A], N, [A, ...P]> : GrowExpRev<A, N, P>;

type MapItemType<T, I> = { [K in keyof T]: I };

export type FixedSizeArray<T, N extends number> = 
  N extends 0 ? [] : MapItemType<GrowExp<[0], N, []>, T>;

let tuple8192: FixedSizeArray<boolean, 8192>;
// let tuple8192: [boolean, boolean, boolean, boolean, boolean, boolean, boolean, 
// boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, boolean, 
// boolean, boolean, ... 8173 more ..., boolean]

let tuple8193: FixedSizeArray<boolean, 8193>; 
// let tuple8193: boolean[]

4 Comments

How do I use this when the number of elements is a variable? If I have N as the number type and "num" as the number, then const arr: FixedArray<number, N> = Array.from(new Array(num), (x,i) => i); gives me "Type instantiation is excessively deep and possibly infinite".
Thanks for getting back to me! If you do come across a solution for variable length, please let me know.
Major caveat with this implementation: passing never, any, or undefined as T also causes "too much recursion" for N > 2 because those types all satisfy extends undefined and will keep the recursion going infinitely
You can now write this in 5 lines. See the significantly shortened version in my comment stackoverflow.com/a/71700658/1843581
19

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

3 Comments

Hey Thomas, what's the purpose of using the readonly keyword in this scenario? (genuine question)
This solution, while more simple, is more limited. Tuple<number,1000> fails due to typescript's recursion limit. The long version of FixedSizeArray will generate tuples up to 8192 in length
This solution doesn't work when the value of N is not known. For instance, in a function generic in N and taking a Tuple<number, N>, it is not recognized that the parameter implements array methods such as map.
18

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

Comments

8

For anyone needing more general solution than the one from @ThomasVo that correctly works with non-literal numbers:

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

I needed to use this type to also work correctly with unknown length arrays.

type FixedLength = LengthArray<string, 3>; // [string, string, string]
type UnknownLength = LengthArray<string, number>; // string[] (instead of [])

2 Comments

This is really an awesome solution. If you find the time it would be cool if you can explain it in detail.
This solution doesn't work when the value of N is not known. For instance, in a function generic in N and taking a Tuple<number, N>, it is not recognized that the parameter implements array methods such as map.
2
const texts: ReadonlyArray<string> & { length: 10 } = [
  'Thats it!',
] as const;

// Types of property  length  are incompatible.
// Type  1  is not assignable to type  10

update:

type FixedArray<TType, TLength extends number> = TType[] & { length: TLength };

const tenNumbers: FixedArray<number, 10> = [123]; // Type 1 is not assignable to type  10

3 Comments

Although this code might answer the question, I recommend that you also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually more helpful and of better quality, and are more likely to attract upvotes.
@MarkRotteveel the code is pretty ok in case of that question imo. The only explanation it would need in production usage.
@AntonS This is beautiful. Simple, elegant use of the type system that is actually readable! Bravo! Take my upvote.
0

you can use this type here:

type ArrayWithLength<Len extends number, T extends unknown, Occ extends T[] = []> = Occ["length"] extends Len
   ? Occ
   : ArrayWithLength<Len, T, [T, ...Occ]>;

this type here uses recursion to create a type of an array with a fixed length.

use case example:

let arr:ArrayWithLength<3 /*the desired length*/, number /*the desired type of the array*/>;
//will result with [number, number, number]

Comments

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.