TypeScript Array: Types, readonly, Tuples and ES2023 Methods

How to declare, type, read, loop over, copy and protect arrays in TypeScript, with tuples and the ES2023 copying methods, tested on TypeScript 7.0 and Node.js 22.

angular-typescript

A TypeScript array is an ordered list of values stored in one variable, where each value has a numeric index starting at 0. It is the regular JavaScript Array with a type attached, so a string[] accepts only strings and pushing a number into it is a compile error. Arrays grow and shrink at runtime, which is why TypeScript has no separate List type. When Java developers look for a TypeScript list, the array is the answer.

We use an array for any ordered list of data, such as the items in a shopping cart or the rows that a REST API returns for a table.

The following example covers each task in this guide in one snippet, written for TypeScript 7.0 with strict on and Node.js 22. The comments show the value each line produces.

// 1. Create typed arrays
const fruits: string[] = ["apple", "banana", "cherry"];
const scores: Array<number> = [10, 20, 30];

// 2. Read elements
const first = fruits[0];                      // first = "apple"
const last = fruits.at(-1);                   // last = "cherry"
const count = fruits.length;                  // count = 3

// 3. Add and remove at the end
fruits.push("mango");                         // ["apple", "banana", "cherry", "mango"]
const removed = fruits.pop();                 // removed = "mango"

// 4. Copy with a change (ES2023)
const sorted = scores.toSorted((a, b) => b - a);   // sorted = [30, 20, 10]
const updated = scores.with(0, 15);           // updated = [15, 20, 30]

// 5. Filter and find
const big = scores.filter((s) => s > 15);     // big = [20, 30]
const found = scores.find((s) => s > 15);     // found = 20

// 6. Loop
for (const fruit of fruits) {
  console.log(fruit);                         // apple, banana, cherry
}

// 7. Merge into a new array
const all = [...fruits, "grape"];             // all = ["apple", "banana", "cherry", "grape"]

// 8. Read-only array and tuple
const days: readonly string[] = ["Mon", "Tue"];
const person: [string, number] = ["Lokesh", 37];

Notice that toSorted() and with() in step 4 return new arrays, so scores keeps its original values.

In the sections that follow, we declare and type arrays, read elements safely, loop over and copy them, and lock them with the readonly modifier. We also cover tuples and the ES2023 methods (toSorted(), toReversed(), toSpliced() and with()) that return a changed copy instead of modifying the original array. Adding, removing, slicing, filtering and searching each have a detailed article, linked from section 3.

1. Declaring a Typed Array

TypeScript needs to know the element type of every array. We either write the type ourselves or let the compiler infer it from the initial values. Once the type is fixed, every later push(), assignment or method call is checked against it.

1.1. string[] and Array<string>

There are two ways to write an array type, and both mean the same thing.

  • The short form is string[].
  • The generic form is Array<string>, where the element type goes between angle brackets.

Most code and the TypeScript Handbook use the short form; the generic form reads better when the element type is long.

// 1. Two ways to write the type
const fruits: string[] = ["apple", "banana"];
const prices: Array<number> = [5, 3];

// 2. Type inferred from the values
const ages = [37, 35, 40];                    // number[]

// 3. Empty array: write the type
const names: string[] = [];
names.push("Lokesh");                         // names = ["Lokesh"]

For an empty array, we always write the type. Without it, the compiler has nothing to infer from and has to guess the type from later code, so the variable type is not visible where we declare it.

1.2. Arrays With a Fixed Starting Length

Sometimes we need an array of a known size before we have the values, for example a row of counters. The Array constructor with one number creates that many empty slots, not elements with a value. The method fill() puts the same value in every slot, and Array.from() builds each element with a function that receives the index.

const slots = new Array<number>(3);           // [ <3 empty items> ]
const zeros = new Array<number>(3).fill(0);   // zeros = [0, 0, 0]
const evens = Array.from({ length: 3 }, (_, i) => i * 2);   // evens = [0, 2, 4]

The methods forEach() and map() skip empty slots, so we fill the array before using it.

1.3. Mixed Values, Objects and Nested Arrays

An array can hold more than one type. We write a union type (a type that accepts any of several types) inside parentheses, as in (string | number)[]. The parentheses matter. Without them, string | number[] means “a single string, or an array of numbers”, and the compiler rejects an array that contains a string.

// 1. Mixed values: parentheses around the union
const values: (string | number)[] = ["apple", 5];

// 2. Objects
interface Person { name: string; age: number; }
const people: Person[] = [
  { name: "Lokesh", age: 37 },
  { name: "Raj", age: 35 },
];
const raj = people[1].name;                   // raj = "Raj"

// 3. Nested arrays (a grid)
const grid: number[][] = [
  [1, 2],
  [3, 4],
];
const cell = grid[1][0];                      // cell = 3

For the version without parentheses, TypeScript 7.0 reports error TS2322.

src/err.ts(1,7): error TS2322: Type '(string | number)[]' is not assignable to type 'string | number[]'.
  Type '(string | number)[]' is not assignable to type 'number[]'.
    Type 'string | number' is not assignable to type 'number'.
      Type 'string' is not assignable to type 'number'.

Arrays of objects come up in almost every app, for example a list of orders loaded from a REST API. We often sort and group arrays of objects by a property, such as orders by date.

2. Reading Elements by Index

Indexes start at 0, so fruits[0] is the first element and fruits[fruits.length – 1] is the last. The at() method also accepts negative numbers, which count from the end, so at(-1) returns the last element without the length arithmetic.

const fruits = ["apple", "banana", "cherry"];

// 1. By index (zero-based)
const first = fruits[0];                      // first = "apple"
const outside = fruits[5];                    // outside = undefined, typed string

// 2. From the end with at()
const last = fruits.at(-1);                   // last = "cherry", typed string | undefined

// 3. Length
const count = fruits.length;                  // count = 3

// 4. Destructuring
const [a, b] = fruits;                        // a = "apple", b = "banana"

The outside line shows a gap in the type checking. Reading past the end returns undefined at runtime, but TypeScript still types fruits[5] as string, so a later outside.toUpperCase() compiles and then throws a TypeError. For example, a page that shows the third search result throws that TypeError when the search returns only two results.

The at() method returns the more accurate type string | undefined. To get the same check for the bracket syntax, we turn on the noUncheckedIndexedAccess option in tsconfig.json. The option is not part of strict, because it also adds undefined to indexes that we know are valid.

3. Adding, Removing, Slicing and Searching

These everyday tasks each have a dedicated article with every variant and its pitfalls. For each task, the table names the main methods, and the last column says whether the method changes the original array. That column matters in shared state such as React state or a function argument, where changing the caller’s array causes bugs that are hard to trace.

TaskMethodsChanges the original?Detailed article
Add elementspush(), unshift(), splice(), spread, concat()push(), unshift(), splice(): yesAdd or append items
Remove elementspop(), shift(), splice(), filter(), toSpliced()pop(), shift(), splice(): yesRemove an item
Take a part of an arrayslice()NoArray slice()
Keep elements that matchfilter()NoArray filter()
Find one element or its indexfind(), findIndex(), findLast(), findLastIndex()NoArray find()
Transform every elementmap(), flatMap()Nomap() vs flatMap()

Two more methods answer yes-or-no questions.

  • The method includes(value) checks whether a value is in the array.
  • The method some(fn) checks whether at least one element passes a test.

Both return a boolean and are cheaper to read than a find() followed by a check.

4. Changing a Copy With ES2023 Methods

The older methods sort(), reverse() and splice() modify the array they are called on. ES2023 added a copying version of each one, plus with() for replacing a single element. These methods return a new array and leave the original untouched, so we no longer need to write […scores].sort(). For example, a leaderboard component that sorts the scores it gets from its parent calls toSorted(), because sort() would reorder the parent’s array as well.

const scores = [30, 10, 20];

// 1. Sort a copy
const sorted = scores.toSorted((a, b) => a - b);   // sorted = [10, 20, 30]

// 2. Reverse a copy
const reversed = scores.toReversed();         // reversed = [20, 10, 30]

// 3. Remove or insert in a copy
const spliced = scores.toSpliced(1, 1);       // spliced = [30, 20]

// 4. Replace one element in a copy
const changed = scores.with(0, 99);           // changed = [99, 10, 20]

// scores is unchanged: [30, 10, 20]

Each copying method has the same parameters as its older partner, which makes switching low-risk. The method with(index, value) also accepts a negative index such as -1 for the last element, and it throws a RangeError when the index is outside the array, while a plain assignment creates empty slots without an error.

Changes the originalReturns a new array (ES2023)
sort(compareFn)toSorted(compareFn)
reverse()toReversed()
splice(start, deleteCount, …items)toSpliced(start, deleteCount, …items)
arr[i] = valuewith(i, value)

These methods work in Node.js 20 and later and in all current browsers. In TypeScript, the lib (or target) option in tsconfig.json must be ES2023 or later. With an older lib, the compiler reports error TS2550.

src/err.ts(2,23): error TS2550: Property 'toSorted' does not exist on type 'number[]'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2023' or later.

One trap applies to both sort() and toSorted(). Without a compare function, they convert the elements to strings and compare them as text, so numbers end up in the wrong order. For numbers, we always pass (a, b) => a – b.

const nums = [10, 9, 1];

const wrong = nums.toSorted();                // wrong = [1, 10, 9]
const right = nums.toSorted((a, b) => a - b); // right = [1, 9, 10]

ES2023 also added findLast() and findLastIndex(), which search from the end of the array and otherwise work like find() and findIndex().

5. Looping Over an Array

For most loops, for…of is the best choice. It gives us each value with its element type and works with break and continue. When we also need the index, entries() returns [index, value] pairs that we split with destructuring.

const fruits = ["apple", "banana"];

// 1. Values
for (const fruit of fruits) {
  console.log(fruit);                         // apple, banana
}

// 2. Index and value
for (const [i, fruit] of fruits.entries()) {
  console.log(i, fruit);                      // 0 apple, 1 banana
}

// 3. forEach
fruits.forEach((fruit, i) => console.log(i, fruit));   // 0 apple, 1 banana

// 4. for...in gives string keys, not values
for (const key in fruits) {
  console.log(typeof key, key);               // string 0, string 1
}

The forEach() method cannot be stopped early with break, and an await inside its callback does not pause the loop. For example, saving each cart item with await inside forEach() starts all the requests at once. The for…in loop is meant for object properties. On an array, it returns the indexes as strings, so we avoid it. The classic for loop with a counter, for (let i = 0; i < fruits.length; i++), still works when we need to step by two or walk backwards.

6. Copying and Merging Arrays

Assigning an array to another variable copies only the reference, so both variables point to the same array. To get a separate array, we use the spread operator (…), which also merges several arrays into one. The method concat() does the same merge and is useful when the arrays come from method calls.

const fruits = ["apple", "banana"];
const more = ["cherry"];

// 1. Shallow copy
const copy = [...fruits];                     // copy = ["apple", "banana"]
copy.push("mango");                           // fruits stays ["apple", "banana"]

// 2. Merge
const merged = [...fruits, ...more];          // merged = ["apple", "banana", "cherry"]
const joined = fruits.concat(more);           // joined = ["apple", "banana", "cherry"]

The spread copy is shallow. It creates a new array, but objects inside it are shared with the original. Changing a property through the copy changes it in the original as well. For a deep copy, structuredClone() copies the array and every object in it. It is available as a global function in Node.js 17 and later and in all current browsers.

const people = [{ name: "Lokesh", age: 37 }];

const shallow = [...people];
shallow[0].age = 38;                          // people[0].age = 38 too

const deep = structuredClone(people);
deep[0].age = 40;                             // people[0].age stays 38

A deep copy costs more time and memory, and structuredClone() cannot copy functions or class methods. We use structuredClone() only when the copy needs independent nested objects, such as an edit form that must not change the original order until the user clicks Save.

7. Read-Only Arrays

A readonly array type removes every method that changes the array, such as push(), pop(), splice() and sort(), and it blocks assignment by index. We use it for function parameters and shared constants, so the compiler tells us when some code tries to change data it does not own. There are three ways to get a read-only array.

// 1. Two ways to write a read-only array type
const days: readonly string[] = ["Mon", "Tue"];
const months: ReadonlyArray<string> = ["Jan", "Feb"];

// 2. as const: read-only tuple of literal types
const sizes = ["S", "M", "L"] as const;       // readonly ["S", "M", "L"]

// 3. Copying methods still work
const sortedDays = days.toSorted();           // sortedDays = ["Mon", "Tue"]
const withWed = [...days, "Wed"];             // withWed = ["Mon", "Tue", "Wed"]

The as const assertion goes further than readonly. It also keeps the exact values as literal types, so sizes can only ever contain “S”, “M” and “L”. With typeof sizes[number], we turn the sizes array into a union of literal types. Any attempt to change a read-only array fails at compile time.

const days: readonly string[] = ["Mon", "Tue"];

days.push("Wed");
days[0] = "Sun";
days.sort();
src/err.ts(3,6): error TS2339: Property 'push' does not exist on type 'readonly string[]'.
src/err.ts(4,1): error TS2542: Index signature in type 'readonly string[]' only permits reading.
src/err.ts(5,6): error TS2339: Property 'sort' does not exist on type 'readonly string[]'.

The readonly modifier exists only in the type system. The compiled JavaScript is a normal array, and code that ignores the types can still change it. When runtime protection is needed, Object.freeze() blocks changes at runtime as well, and TypeScript types its result as a read-only array.

8. Tuples: Fixed-Length Arrays

A tuple is an array type with a fixed number of elements, where each position has its own type. The type [string, number] describes a pair such as a name and an age. Tuples are handy for small groups of values that belong together, such as the [key, value] pairs of a Map or the two values a function returns.

// 1. Fixed length, a type per position
const person: [string, number] = ["Lokesh", 37];
const age = person[1];                        // age = 37, typed number

// 2. Labeled elements and an optional element
type Point = [x: number, y: number, z?: number];
const flat: Point = [1, 2];
const solid: Point = [1, 2, 3];

// 3. Destructuring
const [name, years] = person;                 // name = "Lokesh", years = 37

Reading a position that does not exist is a compile error, so person[2] fails with “Tuple type ‘[string, number]’ of length ‘2’ has no element at index ‘2’”. One gap remains, because a tuple is still an array at runtime and the compiler allows person.push(40) without an error. Writing the type as readonly [string, number] closes that gap.

Labels such as x and y change nothing at runtime; they only make the type and editor hints easier to read. Tuple types also support rest elements, such as [string, …number[]] for a name followed by any number of scores.

9. Running the Array Examples

Every snippet in this guide is part of a small Node.js project in the TypeScript-Examples repository. Each file in src holds one section, and src/index.ts runs them in order and prints the values from the comments. The project pins TypeScript 7.0.2 and requires Node.js 22 or newer.

The tsconfig.json sets lib to ES2024, which includes the ES2023 copying methods, and turns on strict mode.

{
  "compilerOptions": {
    "target": "es2024",
    "lib": ["es2024"],
    "module": "nodenext",
    "strict": true,
    "rootDir": ".",
    "outDir": "dist",
    "types": ["node"],
    "skipLibCheck": true
  },
  "include": ["src"]
}

Inside the arrays folder, we install the compiler and run the project.

npm install
npm start

10. Conclusion

A TypeScript array is a JavaScript array with a checked element type, written as string[] or Array<string>, and it serves as the list type of the language. The at() method and the noUncheckedIndexedAccess option protect us from reading past the end. The ES2023 methods toSorted(), toReversed(), toSpliced() and with() change a copy instead of the original, and they pair well with readonly arrays, which block every changing method at compile time. Tuples cover the case of a short, fixed list with a type per position.

11. References

The MDN pages describe the runtime behavior of each method, and the TypeScript Handbook covers the array, read-only and tuple types.

Happy Learning !!

Source Code on Github

Leave a Comment

  1. I hate when they explain badly .. typescript sucks .. there is no information .. it makes no sense to explain array of one dimension .

  2. how would I create a type for an array of vectors such as

    var myArray = [ [1,2,3], [2,3,4], [3,4,5] ]

      • Different data types can be stored provided the storing array is of type any, eg.

        array1: number[] = [1,2,3];
        arraystr: string[] = [“one”, “two”];
        array3: any[] = […this.array1, …this.arraystr];

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.