Marius Schulz
Marius Schulz
Front End Engineer

Read-Only Array and Tuple Types in TypeScript

TypeScript 3.4 added a bit of syntactic sugar to the language that makes it easier to work with read-only array and tuple types. We can now use the readonly modifier to create read-only array types (such as readonly string[]) or read-only tuple types (such as readonly [number, number]).

Read-Only Array Types in TypeScript

Let's assume we've defined the following intersperse function:

function intersperse<T>(elements: T[], separator: T): T[] {
  const newElements = [];
  for (let i = 0; i < elements. length; i++) {
    if (i !== 0) {
      newElements.push(separator);
    }
    newElements.push(elements[i]);
  }
  return newElements;
}

The intersperse function accepts an array of elements of some type T and a separator value of the same type T. It returns a new array of elements with the separator value interspersed in between each of the elements. In a way, the intersperse function is similar to the Array.prototype.join() method, except that it returns an array of the same type instead of a string.

Here are some usage examples of our intersperse function:

intersperse(["a", "b", "c"], "x")
// ["a", "x", "b", "x", "c"]

intersperse(["a", "b"], "x")
// ["a", "x", "b"]

intersperse(["a"], 0)
// ["a"]

intersperse([], 0)
// []

Let's now create an array that is annotated to be of type ReadonlyArray<string>, a read-only array type:

const values: ReadonlyArray<string> = ["a", "b", "c"];

This means that we don't intend for this array to be mutated. TypeScript's type checker will produce an error if we try to write to the array or call mutating array methods such as push(), pop(), or splice():

values[0] = "x";      // Type error
values.push("x");     // Type error
values.pop();         // Type error
values.splice(1, 1);  // Type error

Alternatively, we could've used the new readonly modifier to type our values array as a read-only array:

const values: readonly string[] = ["a", "b", "c"];

ReadonlyArray<string> and readonly string[] represent the same type; you can pick whichever syntax you prefer. I like readonly T[] because it's more concise and closer to T[], but your mileage may vary. It's just a matter of preference.

What happens if we now try to pass values to intersperse?

const valuesWithSeparator = intersperse(values, "x");

TypeScript gives us another type error!

Argument of type 'readonly string[]' is not assignable to parameter of type 'string[]'.
  The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.

The type checker points out that the mutable array type string[] cannot be assigned to the read-only array type readonly string[]. Here, the potential problem is that our intersperse function could call mutating methods on the elements parameter. That would violate the intended read-only behavior of the values array.

We can make the type error go away by typing the elements parameter as a read-only array. By doing that, we're indicating that our intersperse function is not going to mutate the elements array:

function intersperse<T>(elements: readonly T[], separator: T): T[] {
  const newElements = [];
  for (let i = 0; i < elements. length; i++) {
    if (i !== 0) {
      newElements.push(separator);
    }
    newElements.push(elements[i]);
  }
  return newElements;
}

const values: readonly string[] = ["a", "b", "c"];
const valuesWithSeparator = intersperse(values, "x");

If you're writing a pure function that accepts an array as a parameter, I would recommend that you annotate that array parameter to be read-only. That way, your function can be called with mutable and read-only arrays alike. In addition, TypeScript will help you prevent accidental mutation of those parameters within the function.

If you want to experiment with read-only array types and play around with the above type annotations, I've prepared this TypeScript playground for you.

Read-Only Tuple Types in TypeScript

Similar to read-only array types, TypeScript lets us create read-only tuple types using the readonly modifier:

const point: readonly [number, number] = [0, 0];

Any attempt to mutate a value of a read-only tuple type will result in a type error:

point[0] = 1;        // Type error
point.push(0);       // Type error
point.pop();         // Type error
point.splice(1, 1);  // Type error

For tuple types, there's no equivalent of the ReadonlyArray type. You'll have to rely on the readonly modifier to make a tuple type read-only.

Again, if you want to play around with tuple types and the readonly modifier, feel free to use this TypeScript playground.

This post is part of the TypeScript Evolution series.