Creating a Fixed-length Array in TypeScript

Seeking assistance with TypeScript types, I have a particular question.

When declaring a type for an array as follows...

position: Array<number>;

...it allows for creating an array of any length. However, if you need an array containing numbers of a specific length, such as 3 for x, y, z components, is it possible to define a type for a fixed-length array like this?

position: Array<3>

Any guidance or clarification would be greatly appreciated!

Answer №1

In JavaScript, you can create an array by specifying its initial length using the constructor:

let newArray = new Array<string>(5);
console.log(newArray); // [undefined × 5]

Keep in mind that this initial size is flexible, and you can add or remove elements as needed:

newArray.push("Hello");
console.log(newArray); // [undefined × 5, "Hello"]

For more strict type checking, TypeScript offers tuple types to define arrays with specific lengths and types:

let tupleArray: [number, number, number];

tupleArray = [1, 2, 3]; // valid
tupleArray = [1, 2]; // Error: Type '[number, number]' is not assignable to type '[number, number, number]'
tupleArray = [1, 2, "3"]; // Error: Type '[number, number, string]' is not assignable to type '[number, number, number]'

Answer №2

The Tuple Technique :

This method offers a precise FixedLengthArray (also known as SealedArray) type that is based on Tuples.

Example of Syntax :

// Array with 3 strings
let bar : FixedLengthArray<[string, string, string]> 

This approach emphasizes safety by preventing access to indexes beyond the specified boundaries.

How it Works :

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> > }

Testing Scenario :

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

// Tests for array declaration
myTupleArray = [ 'a', 'b', 'c' ]  // ✅ OK
myTupleArray = [ 'a', 'b', 123 ]  // ✅ TYPE ERROR
myTupleArray = [ 'a' ]            // ✅ LENGTH ERROR
myTupleArray = [ 'a', 'b' ]       // ✅ LENGTH ERROR

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

// Methods impacting array length
myTupleArray.push('foo')          // ✅ MISSING METHOD ERROR
myTupleArray.pop()                // ✅ MISSING METHOD ERROR

// Manipulating array length directly
myTupleArray.length = 123         // ✅ READ-ONLY ERROR

// Destructuring
var [ alpha ] = myTupleArray          // ✅ OK
var [ alpha, beta ] = myTupleArray       // ✅ OK
var [ alpha, beta, gamma ] = myTupleArray    // ✅ OK
var [ alpha, beta, gamma, delta ] = myTupleArray // ✅ INVALID INDEX ERROR

(*) To use this solution, make sure to enable the noImplicitAny typescript configuration directive as advised.


The Array-Like Technique :

This approach acts as an extension of the Array type, allowing for an additional parameter denoting the length of the array. It is not as stringent and secure as the Tuple-based solution.

Example of Syntax :

let baz: FixedLengthArray<string, 3> 

Note that this method does not prevent unauthorized access to indexes outside the defined boundaries or altering their values.

Technical 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 
    [ index : number ] : T
    [Symbol.iterator]: () => IterableIterator<T>   
  }

Testing Scenario :

var myArrayLike: FixedLengthArray<string,3>

// Tests for array declaration
myArrayLike = [ 'a', 'b', 'c' ]  // ✅ OK
myArrayLike = [ 'a', 'b', 123 ]  // ✅ TYPE ERROR
myArrayLike = [ 'a' ]            // ✅ LENGTH ERROR
myArrayLike = [ 'a', 'b' ]       // ✅ LENGTH ERROR

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

// Methods impacting array length
myArrayLike.push('foo')          // ✅ MISSING METHOD ERROR
myArrayLike.pop()                // ✅ MISSING METHOD ERROR

// Direct manipulation of array length
myArrayLike.length = 123         // ✅ READ-ONLY ERROR

// Destructuring
var [ one ] = myArrayLike          // ✅ OK
var [ one, two ] = myArrayLike       // ✅ OK
var [ one, two, three ] = myArrayLike    // ✅ OK
var [ one, two, three, four ] = myArrayLike // ❌ SHOULD FAIL

Answer №3

The original response was crafted some time back, using typescript version 3.x. Since then, typescript has advanced to version 4.94, lifting certain limitations that existed. Additionally, modifications were made to address issues raised in the comments.

Initial Response

It is possible to achieve this with the current version of 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:

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

During typescript 3.x era, this approach allowed for creating tuples of small sizes up to 20 elements. However, attempting larger sizes resulted in a "Type instantiation is excessively deep and possibly infinite" error, as highlighted by @Micha Schwab in the comment below. This led to exploring a more efficient array growth method, leading to Edit 1.

EDIT 1: Handling Larger Sizes

This revised method can handle larger tuple sizes through exponential array growth until reaching the nearest 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 enhancement enabled handling tuple sizes up to 2^15, though performance started degrading noticeably beyond 2^13. Furthermore, it struggled with `any`, `never`, and `undefined` types, triggering an infinite recursion loop due to them satisfying the `extends undefined ?` condition which checked for exceeding array index, as identified by @Victor Zhou's comment.

EDIT 2: Addressing Specific Tuple Types

The "exponential array growth" technique had difficulties with tuples containing `any`, `never`, or `undefined`. One workaround involved preparing a tuple with a generic type before transforming it to the desired item type based on the requested size.

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]

With the current typescript version now at 4.94, it's time to summarize and refine the code.

EDIT 3: Introducing Typescript 4.94 Updates

The initial FixedArray type can now be simplified as follows:

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 version can accommodate sizes up to 999.

let tuple999: FixedArray<boolean, 999>; 
// let tuple999: [boolean, boolean, ... (repeated 980 times), boolean]

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

To ensure safety, we add a safeguard to return an array of type T if the 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" strategy can now cater to tuple sizes up to 8192 (2^13).

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

The updated version, incorporating a safeguard at size 8192, is presented 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, ... (repeated 8173 times), boolean]

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

Answer №4

Joining the conversation a bit late, here's a method for dealing with read-only arrays using [] as const:

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

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

If you try to add or remove strings from the const a, you will encounter 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): Exciting upcoming TS feature - satisfies defined here

Answer №5

Using the latest version of Typescript, which is v4.6, I have condensed a solution inspired by Tomasz Gawel's helpful response.

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

// example
const x: Tuple<number,3> = [1,2,3];
x; // results in [number, number, number]
x[0]; // results in number

While there are other methods to enforce the length property, they may not be as visually appealing.

// To keep it brief, avoid this
type Tuple<T, N> = { length: N } & readonly T[];
const x : Tuple<number,3> = [1,2,3]

x; // resolves as { length: 3 } | number[], creating confusion
x[0]; // resolves as number | undefined, which is inaccurate

Answer №6

Seeking a more versatile solution for handling non-literal numbers, different from what @ThomasVo provided:

type CustomArray<
        X,
        Y extends number,
        Z extends X[] = []
    > = number extends Y
        ? X[]
        : Z['length'] extends Y
        ? Z
        : CustomArray<X, Y, [X, ...Z]>;

I found it necessary to utilize this type in order to effectively manage arrays of unknown lengths.

type SpecificLength = CustomArray<string, 3>; // [string, string, string]
type AmbiguousLength = CustomArray<string, number>; // string[] (instead of [])

Answer №7

Here's something that might be helpful:

type isZero<N extends number> = N extends 0 ? true : false;
type isNegative<N extends number> = `${N}` extends `-${string}` ? true : false;
type isDecimal<N extends number> = `${N}` extends `${string}.${string}` ? true : false;

type _Vector<N extends number, T extends unknown[] = []> = isZero<N> extends true
    ? never
    : isNegative<N> extends true
    ? never
    : isDecimal<N> extends true
    ? never
    : T["length"] extends N
    ? T
    : _Vector<N, [...T, number]>;

export type Vector<N extends number> = _Vector<N>;

let vec1: Vector<1>;    // [number]
let vec2: Vector<2>;    // [number, number]
let vec3: Vector<3>;    // [number, number, number]

let err1: Vector<2.5>;  // never
let err2: Vector<0>;    // never;
let err3: Vector<-1>;   // never

I received assistance from ChatGPT for this solution, with some modifications on my end.

Answer №8

const messages: ReadonlyArray<string> & { length: 10 } = [
  'That is the end result!',
] as const;

// The types of property  length  do not match.
// Type  1  cannot be assigned to type  10

update:

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

const tenValues: LimitedArray<number, 10> = [123]; // Type 1 cannot be assigned to type  10

Answer №9

Try this code snippet:

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

This unique type uses recursive logic to generate an array with a specified length.

Example of how to use it:

let myArr:FixedLengthArray<3 /*length*/, string /*data type*/>;
// Output: [string, string, string]

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Are there any potential performance implications to passing an anonymous function as a prop?

Is it true that both anonymous functions and normal functions are recreated on every render? Since components are functions, is it necessary to recreate all functions every time they are called? And does using a normal function offer any performance improv ...

How can I update a dropdown menu depending on the selection made in another dropdown using Angular

I am trying to dynamically change the options in one dropdown based on the selection made in another dropdown. ts.file Countries: Array<any> = [ { name: '1st of the month', states: [ {name: '16th of the month&apos ...

Tips for creating Material UI elements using React and Typescript

I am looking to extend a component from the Material UI library by creating a custom component that encapsulates specific styles, such as for a modal wrapper. However, I feel that my current solution involving the props interface may not be ideal. Is the ...

Is it possible to find a more efficient approach than calling setState just once within useEffect?

In my react application, I find myself using this particular pattern frequently: export default function Profile() { const [username, setUsername] = React.useState<string | null>(null); React.useEffect(()=>{ fetch(`/api/userprofil ...

Xtermjs does not support copying and pasting functionalities

I'm struggling to enable the copy & paste feature in my terminal using xterm.js APIs. My goal is to allow users to copy strings from the clipboard. Currently, I have implemented the following code: this.term.onKey((key) => { if (key.domEven ...

Data not reflecting changes in component when field is modified?

I currently have a table on my web page that displays data fetched from an array of objects. The data is collected dynamically through websockets by listening to three different events. User can add an entry - ✅ works perfectly User can modify ...

Having trouble installing dependencies in a React project with TypeScript due to conflicts

I encountered a TypeScript conflict issue whenever I tried to install any dependency in my project. Despite attempting various solutions such as updating dependencies, downgrading them, and re-installing by removing node_modules and package-lock.json, the ...

Disabling ESLint errors is not possible within a React environment

I encountered an eslint error while attempting to commit the branch 147:14 error Expected an assignment or function call and instead saw an expression @typescript-eslint/no-unused-expressions I'm struggling to identify the issue in the code, even ...

Managing Import Structure in Turborepo/Typescript Package

I am currently working on creating a range of TypeScript packages as part of a Turborepo project. Here is an example of how the import structure for these packages looks like: import { Test } from "package-name" import { Test } from "package ...

D3: Utilizing multiple line charts in Zoom to effectively merge their data arrays

I encountered an issue with my charts while trying to implement zoom functionality. Initially, I have a chart displaying random data and when I add another chart on top of it, the zooming feature copies data from the second chart onto the first one instead ...

Obtain a value that is not defined

Good day, I am encountering an issue with my data not accepting an undefined value. Below is the code snippet: interface IModalContatos { dados: IContatos; onSave(dados: IContatos): void; onClose(): void; } When passing this data to my modal, I rece ...

The process of inserting data into MongoDB using Mongoose with TypeScript

Recently, I encountered an issue while trying to insert data into a MongoDB database using a TypeScript code for a CRUD API. The problem arises when using the mongoose package specifically designed for MongoDB integration. import Transaction from 'mon ...

The type definition file for '@wdio/globals/types' is nowhere to be found

I'm currently utilizing the webdriverio mocha framework with typescript. @wdio/cli": "^7.25.0" NodeJs v16.13.2 NPM V8.1.2 Encountering the following error in tsconfig.json JSON schema for the TypeScript compiler's configuration fi ...

Issue: The parameter "data" is not recognized as a valid Document. The input does not match the requirements of a typical JavaScript object

I encountered the following issue: Error: Argument "data" is not a valid Document. Input is not a plain JavaScript object. while attempting to update a document using firebase admin SDK. Below is the TypeScript snippet: var myDoc = new MyDoc(); myDo ...

Display JSX using the material-ui Button component when it is clicked

When I click on a material-ui button, I'm attempting to render JSX. Despite logging to the console when clicking, none of the JSX is being displayed. interface TileProps { address?: string; } const renderDisplayer = (address: string) => { ...

What are some strategies for debugging a live Angular2/Typescript application?

As I start the exciting journey of creating a new app with Angular2 and Typescript, two technologies that I have never used together before (although I do have experience using them individually), a question arises in my mind. How can I effectively debug ...

How to Delete an Item from an Array in BehaviorSubject Using Angular/Typescript

I have encountered an issue while trying to delete a specific element from my array upon user click. Instead of removing the intended item only, it deletes all elements in the array. I attempted to use splice method on the dataService object, but I'm ...

A more efficient method for importing numerous 'export' statements using ES6 or TypeScript

Currently, I am utilizing D3.js V4 with the module and my goal is to import multiple modules into a singular namespace. The code snippet provided below showcases my current approach, but I am curious if there is a more efficient method available. const d3 ...

The MongoDB entity is failing to save, despite a successful attempt

This Represents the Model var mongoose = require('mongoose'), Schema = mongoose.Schema; var PostSchema = new Schema({ post_author: { type: Schema.ObjectId, ref: "User" }, post_text: String, total_comments ...

Exploring Typescript's null chain and narrowing down types

Recently, I encountered a situation where typescript seems to be incorrectly narrowing the given type. (value: number[] | null) => { if ((value?.length ?? 0) > 0) value[0]; }; Even though the condition will not be true if the value is null, in th ...