Improvements for ReadonlyArray and readonly tuples

TypeScript 3.4 makes it a little bit easier to use read-only array-like types.

A new syntax for ReadonlyArray

The ReadonlyArray type describes Arrays that can only be read from.Any variable with a reference to a ReadonlyArray can’t add, remove, or replace any elements of the array.

  1. function foo(arr: ReadonlyArray<string>) {
  2. arr.slice(); // okay
  3. arr.push("hello!"); // error!
  4. }

While it’s good practice to use ReadonlyArray over Array when no mutation is intended, it’s often been a pain given that arrays have a nicer syntax.Specifically, number[] is a shorthand version of Array<number>, just as Date[] is a shorthand for Array<Date>.

TypeScript 3.4 introduces a new syntax for ReadonlyArray using a new readonly modifier for array types.

  1. function foo(arr: readonly string[]) {
  2. arr.slice(); // okay
  3. arr.push("hello!"); // error!
  4. }

readonly tuples

TypeScript 3.4 also introduces new support for readonly tuples.We can prefix any tuple type with the readonly keyword to make it a readonly tuple, much like we now can with array shorthand syntax.As you might expect, unlike ordinary tuples whose slots could be written to, readonly tuples only permit reading from those positions.

  1. function foo(pair: readonly [string, string]) {
  2. console.log(pair[0]); // okay
  3. pair[1] = "hello!"; // error
  4. }
The same way that ordinary tuples are types that extend from Array - a tuple with elements of type T1, T2, … Tn extends from Array< T1T2Tn > - readonly tuples are types that extend from ReadonlyArray. So a readonly tuple with elements T1, T2, … Tn extends from ReadonlyArray< T1T2Tn >.

readonly mapped type modifiers and readonly arrays

In earlier versions of TypeScript, we generalized mapped types to operate differently on array-like types.This meant that a mapped type like Boxify could work on arrays and tuples alike.

  1. interface Box<T> { value: T }
  2. type Boxify<T> = {
  3. [K in keyof T]: Box<T[K]>
  4. }
  5. // { a: Box<string>, b: Box<number> }
  6. type A = Boxify<{ a: string, b: number }>;
  7. // Array<Box<number>>
  8. type B = Boxify<number[]>;
  9. // [Box<string>, Box<number>]
  10. type C = Boxify<[string, boolean]>;

Unfortunately, mapped types like the Readonly utility type were effectively no-ops on array and tuple types.

  1. // lib.d.ts
  2. type Readonly<T> = {
  3. readonly [K in keyof T]: T[K]
  4. }
  5. // How code acted *before* TypeScript 3.4
  6. // { readonly a: string, readonly b: number }
  7. type A = Readonly<{ a: string, b: number }>;
  8. // number[]
  9. type B = Readonly<number[]>;
  10. // [string, boolean]
  11. type C = Readonly<[string, boolean]>;

In TypeScript 3.4, the readonly modifier in a mapped type will automatically convert array-like types to their corresponding readonly counterparts.

  1. // How code acts now *with* TypeScript 3.4
  2. // { readonly a: string, readonly b: number }
  3. type A = Readonly<{ a: string, b: number }>;
  4. // readonly number[]
  5. type B = Readonly<number[]>;
  6. // readonly [string, boolean]
  7. type C = Readonly<[string, boolean]>;

Similarly, you could write a utility type like Writable mapped type that strips away readonly-ness, and that would convert readonly array containers back to their mutable equivalents.

  1. type Writable<T> = {
  2. -readonly [K in keyof T]: T[K]
  3. }
  4. // { a: string, b: number }
  5. type A = Writable<{
  6. readonly a: string;
  7. readonly b: number
  8. }>;
  9. // number[]
  10. type B = Writable<readonly number[]>;
  11. // [string, boolean]
  12. type C = Writable<readonly [string, boolean]>;

Caveats

Despite its appearance, the readonly type modifier can only be used for syntax on array types and tuple types.It is not a general-purpose type operator.

  1. let err1: readonly Set<number>; // error!
  2. let err2: readonly Array<boolean>; // error!
  3. let okay: readonly boolean[]; // works fine

You can see more details in the pull request.