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!
To declare a fixed length array in TypeScript, we can use the Array constructor or create a tuple. const arr = new Array<number>(3); const tuple: [number, number, number] = [1, 2, 3]; to create the arr array which is an array of numbers with new Array<number>(3) .
Fixed arrays have a fixed size which cannot be changed at run-time. These are also known as Static Arrays. An array is declared by including parentheses after the array name or identifier. An integer is placed within the parentheses, defining the number of elements in the array.
Arrays can be declared and initialized separately. let fruits: Array<string>; fruits = ['Apple', 'Orange', 'Banana']; let ids: Array<number>; ids = [23, 34, 100, 124, 44]; An array in TypeScript can contain elements of different data types using a generic array type syntax, as shown below.
An array is a container object that holds a fixed number of values of a single type. The length of an array is established when the array is created. After creation, its length is fixed.
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]'
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)
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
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]]>;
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With