Массивоподобные readonly типы, ReadonlyArray, ReadonlyMap, ReadonlySet

Чем меньше шансов случайного изменения значений определенных в объектных типах, тем больше программа защищена от ошибок во время выполнения. Очередным шагоv в этом направлении стали неизменяемые массивоподобные типы ReadonlyArray<T>, ReadonlyMap<K, V>, ReadonlySet<T>, а также механизм указания модификатора readonly в аннотации типа.

Массивоподобные readonly типы (модифицировать непосредственно в аннотации типа)

TypeScript реализует механизм позволяющий определять массивы и кортежи, как неизменяемые структуры данных. Для этого к типу указанному в аннотации типа добавляется модификатор readonly.

ts
let array: readonly string[] = ['Kent', 'Clark']; // Массив
let tuple: readonly [string, string] = ['Kent', 'Clark']; // Кортеж

Элементы массивоподобных структур, определенных как readonly, невозможно заменить или удалить. Кроме того, в подобные структуры невозможно добавить новые элементы. Иными словами, у массивоподобных readonly типов отсутствуют признаки предназначенные для изменения их содержимого.

В случае объявления readonly массива становится невозможно изменить его элементы с помощью индексной сигнатуры (array[...])

ts
let array: readonly string[] = ['Kent', 'Clark']; // Массив
array[0] = 'Wayne'; // Error, -> Index signature in type 'readonly string[]' only permits reading.
array[array.length] = 'Batman'; // Error ->  Index signature in type 'readonly string[]' only permits reading.

Помимо этого, у readonly массива отсутствуют методы, с помощью которых можно изменить элементы массива.

ts
let array: readonly string[] = ['Kent', 'Clark'];
array.push('Batman'); // Error ->  Property 'push' does not exist on type 'readonly string[]'.
array.shift(); // Error -> Property 'shift' does not exist on type 'readonly string[]'.

array.indexOf('Kent'); // Ok
array.map(item => item); // Ok

С учетом погрешности на известные различия между массивом и кортежем, справедливо утверждать, что правила для readonly массива справедливы и для readonly кортежа. Помимо того, что невозможно изменить или удалить слоты кортежа, он также теряет признаки массива, которые способны привести к его изменению.

ts
let tuple: readonly [string, string] = ['Kent', 'Clark'];
tuple[0] = 'Wayne'; // Error -> Cannot assign to '0' because it is a read-only property.

tuple.push('Batman'); // Error -> Property 'push' does not exist on type 'readonly [string, string]'.
tuple.shift(); // Error -> Property 'shift' does not exist on type 'readonly [string, string]'.

tuple.indexOf('Kent'); // Ok
tuple.map(item => item); // Ok

Также не будет лишним упомянуть, что массив или кортеж указанный в аннотации с помощью расширенного типа Readonly<T>, расценивается выводом типов как помечанный модификатором readonly.

ts
// type A = readonly number[];
type A = Readonly<number[]>;

// type B = readonly [string, boolean];
type B = Readonly<[string, boolean]>;

Благодаря данному механизму в сочетании с механизмом множественного распространения (spread), становится возможным типизировать сложные сценарии, одним из которых является реализация известной всем функции concat способной объединить не только массивы, но и кортежи.

ts
type A = readonly unknown[];

function concat<T extends A, U extends A>(a: T, b: U): [...T, ...U] {
    return [...a, ...b];
}

// let v0: number[]
let v0 = concat([0, 1], [2, 3]);

// let v1: [0, 1, 2, 3]
let v1 = concat([0, 1] as const, [2, 3] as const);

// let v2: [0, 1, ...number[]]
let v2 = concat([0, 1] as const, [2, 3]);

// let v3: number[]
let v3 = concat([0, 1], [2, 3] as const);

Напоследок стоит упомянуть, что вывод типов расценивает readonly массив как принадлежащий к интерфейсу ReadonlyArray<T>, речь о котором пойдет далее.

ReadonlyArray<T> (неизменяемый массив)

Расширенный тип ReadonlyArray<T> предназначен для создания неизменяемых массивов. ReadonlyArray<T> запрещает изменять значения массива, используя индексную сигнатуру array[n].

ts
let array: ReadonlyArray<number> = [0, 1, 2];

array[0] = 1; // Error -> Index signature in type 'readonly number[]' only permits reading.
array[array.length] = 3; // Error -> Index signature in type 'readonly number[]' only permits reading.

Кроме того, тип ReadonlyArray<T> не содержит методы, способные изменить, удалить или добавить элементы.

ts
let array: ReadonlyArray<number> = [0, 1, 2];

array.push(3); // Error -> Property 'push' does not exist on type 'readonly number[]'.
array.shift(); // Error -> Property 'shift' does not exist on type 'readonly number[]'.

array.indexOf(0); // Ok 

ReadonlyMap<K, V> (неизменяемая карта)

Расширенный тип ReadonlyMap<K, V>, в отличие от своего полноценного прототипа, не имеет методов, способных его изменить.

ts
let map: ReadonlyMap<string, number> = new Map([["key", 0]]);

ReadonlySet<T> (неизменяемое множество)

Аналогично другим структурам данных предназначенных только для чтения, расширенный тип ReadonlySet<T> не имеет методов, способных его изменить.

ts
let set: ReadonlySet<number> = new Set([0, 1, 2]);