Jonas Kello
Jonas Kello

Reputation: 1402

How do I declare a read-only array tuple in TypeScript?

We can declare a typed tuple in TypeScript, for example, with the type annotation [string, number]. This means an array of 2 elements where the first element needs to be a string and the second a number.

We can also declare read-only arrays with ReadonlyArray<string> which means a read-only array of strings.

Now I want to have a read-only tuple like in the first example, but I want it to be read-only like in the second example. How would I declare that?

Upvotes: 38

Views: 43919

Answers (8)

Andrew Ross
Andrew Ross

Reputation: 1158

TypeScript Version 5.4.2

if you have an object with as const appended, it will create a readonly object.

use this object to derive a strongly typed tuple from

This should help, check out the playground link as well

type UnknownObj = Record<string | number | symbol, unknown>;

const categories = {
  "0": "Any",
  "9": "General",
  "10": "Entertainment"
} as const;

const example = {
  nested: categories,
  bool: Boolean(0) === false ? true : false,
  myArr: ["mixed", true, 123, ["arr"]]
} as const;

const toReadonlyTuple = <const T extends UnknownObj>(props: T)=> {
  return Object.entries(props).map(([key, val])=> {
    return [key as keyof T, val as T[keyof T]] as const
  }) satisfies (readonly [keyof T, T[keyof T]])[]
}

const toReadOnlyArrOfKeysOrvals = <
  const T extends UnknownObj,
  const V extends "keys" | "vals"
>(
  props: T,
  target: V
) => {
  return Object.entries(props)
    .map(([key, val]) => {
      return [key as keyof T, val as T[keyof T]] as const satisfies readonly [
        keyof T,
        T[keyof T]
      ];
    })
    .map(([key, val]) =>
      target === "keys" ? ([...[key]] as const)[0] : Object.freeze(val)
    ) as  typeof target extends "keys" ? readonly (keyof T)[] : Readonly<T[keyof T]>[];
};

console.log(toReadonlyTuple(example));
console.log(toReadOnlyArrOfKeysOrvals(example, "keys"));
console.log(toReadOnlyArrOfKeysOrvals(example, "vals"))

The generated declaration definitions for either function

declare const toReadonlyTuple: <const T extends UnknownObj>(props: T) => (readonly [keyof T, T[keyof T]])[];

declare const toReadOnlyArrOfKeysOrvals: <const T extends UnknownObj, const V extends "keys" | "vals">(props: T, target: V) => V extends "keys" ? readonly (keyof T)[] : Readonly<T[keyof T]>[];

console output:

[LOG]: [["nested", {
  "0": "Any",
  "9": "General",
  "10": "Entertainment"
}], ["bool", true], ["myArr", ["mixed", true, 123, ["arr"]]]] 
[LOG]: ["nested", "bool", "myArr"] 
[LOG]: [{
  "0": "Any",
  "9": "General",
  "10": "Entertainment"
}, true, ["mixed", true, 123, ["arr"]]] 

Upvotes: 0

Ruochen Jia
Ruochen Jia

Reputation: 9

You can invoke Object.freeze while creating an array to make it readonly.

const arr = Object.freeze(["text", 2, 3]);

This will automatically convert the type to readonly (string | number)[] in TypeScript, and will be readonly during runtime as well.

Upvotes: -1

ggradnig
ggradnig

Reputation: 14189

Solution for TypeScript 3.4+: Const Assertion

With const assertions, the compiler can be told to treat an array or an object as immutable, meaning that their properties are read-only. This also allows the creation of literal tuple types with narrower type inference (i.e. your ["a", "b"] can be of type ["a", "b"], rather than string[] without specifiying the whole thing as a contextual type)

The syntax:

const foo = ["text", 1] as const // or
const foo = <const> ["text", 1]
// typeof foo:
readonly ["text", 1]

It can also be used for object literals:

const myObj = {
  foo: 1,
  bar: ["a", "b"],
  baz: true,
} as const
// typeof myObj:
{ readonly foo: 1, readonly bar: readonly ["a", "b"], readonly baz: true }

Here is the extended information of the corresponding PR.

Upvotes: 26

Mariusz Pawelski
Mariusz Pawelski

Reputation: 28922

From Typescript version 3.4 you can just prefix tuple type with readonly keyword (source).

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.

function foo(pair: readonly [string, string]) {
    console.log(pair[0]);   // okay
    pair[1] = "hello!";     // error
}

Upvotes: 15

aleclarson
aleclarson

Reputation: 19045

As of v3.2.2, there's no perfect way of making a readonly tuple type without converting it to an object that looks like an array, but is not.

The lead architect of TypeScript has said this on the topic of combining Readonly<T> with tuple types.

Here is the best solution I've come up with:

type ReadonlyTuple<T extends any[]> = {
    readonly [P in Exclude<keyof T, keyof []>]: T[P]
} & Iterable<T[number]>

Upvotes: 2

Ruby Tunaley
Ruby Tunaley

Reputation: 371

The accepted answer leaves array mutation methods unaffected, which can cause unsoundness in the following way:

const tuple: Readonly<[number, string]> = [0, ''];
tuple.shift();
let a = tuple[0]; // a: number, but at runtime it will be a string

The code below fixes this issue, and includes Sergey Shandar's destructuring fix. You'll need to use --noImplicitAny for it to work properly.

type ArrayItems<T extends ReadonlyArray<any>> = T extends ReadonlyArray<infer TItems> ? TItems : never;

type ExcludeProperties<TObj, TKeys extends string | number | Symbol> = Pick<TObj, Exclude<keyof TObj, TKeys>>;

type ArrayMutationKeys = Exclude<keyof any[], keyof ReadonlyArray<any>> | number;

type ReadonlyTuple<T extends any[]> = Readonly<ExcludeProperties<T, ArrayMutationKeys>> & {
    readonly [Symbol.iterator]: () => IterableIterator<ArrayItems<T>>;
};

const tuple: ReadonlyTuple<[number, string]> = [0, ''];
let a = tuple[0]; // a: number
let b = tuple[1]; // b: string
let c = tuple[2]; // Error when using --noImplicitAny
tuple[0] = 1; // Error
let [d, e] = tuple; // d: number, e: string
let [f, g, h] = tuple; // Error

Upvotes: 12

Sergey Shandar
Sergey Shandar

Reputation: 2387

Readonly<[string, T]> doesn't allow destruction. For example

const tuple: Readonly<[string, number]> = ["text", 4]

const [n, v] = tuple // error TS2488: Type 'Readonly<[string, number]>' must have a '[Symbol.iterator]()' method that returns an iterator.

So, it's better to use a custom interface

export interface Entry<T> {
    readonly [0]: string
    readonly [1]: T
    readonly [Symbol.iterator]: () => IterableIterator<string|T>
}

For example

const tuple: Entry<number> = ["text", 4]

const [name, value] = tuple // ok
const nameCheck: string = name
const valueCheck: number = value

Upvotes: 4

Arg0n
Arg0n

Reputation: 8423

Since the type [string, number] already is an Array, you can simply use:

Readonly<[string, number]>

Example:

let tuple: Readonly<[string, number]> = ['text', 3, 4, 'another text'];

tuple[0] = 'new text'; //Error (Readonly)

let string1: string = tuple[0]; //OK!
let string2: string = tuple[1]; //Error (Type number)
let number1: number = tuple[0]; //Error (Type string)
let number2: number = tuple[1]; //OK!
let number3: number = tuple[2]; //Error (Type any)

Upvotes: 25

Related Questions