Пошук уроків, статей та іншого контенту
Захистіть масиви від змін і типізуйте операції spread та rest під час роботи з колекціями.
За замовчуванням масив у TypeScript можна змінювати:
const scores: number[] = [10, 20, 30];
scores.push(40);
scores[0] = 15;Іноді функція має лише читати отриману колекцію. У такому разі корисно явно заборонити зміни на рівні типів:
function printScores(scores: readonly number[]): void {
for (const score of scores) {
console.log(score);
}
}
const scores = [10, 20, 30];
printScores(scores);
printScores([5, 15, 25]);Тип readonly number[] означає: «масив чисел, з якого можна читати значення, але не можна змінювати його через це посилання».
Альтернативний запис:
function printScores(scores: ReadonlyArray<number>): void {
console.log(scores.join(", "));
}readonly number[] і ReadonlyArray<number> мають однаковий зміст. Перший варіант зазвичай коротший.
Readonly-масив не підтримує методи, які змінюють початкову колекцію:
function addScore(scores: readonly number[]): void {
// scores.push(100); // Помилка компіляції
// scores[0] = 100; // Помилка компіляції
console.log(scores.length);
console.log(scores[0]);
const doubled = scores.map((score) => score * 2);
const filtered = scores.filter((score) => score >= 50);
console.log(doubled);
console.log(filtered);
}До таких методів належать:
push;
pop;
shift;
unshift;
splice;
sort;
reverse.
Методи, які створюють нову колекцію, дозволені. Наприклад, map, filter і slice не змінюють початковий масив.
const readonlyScores: readonly number[] = [20, 50, 80];
const copiedScores = readonlyScores.slice();
const increasedScores = readonlyScores.map((score) => score + 10);
console.log(copiedScores);
console.log(increasedScores);Результат map, filter або slice зазвичай має звичайний тип масиву, наприклад number[]. Це означає, що нову колекцію можна змінювати:
const readonlyScores: readonly number[] = [20, 50, 80];
const increasedScores = readonlyScores.map((score) => score + 10);
increasedScores.push(100);Звичайний масив можна передати туди, де очікується readonly-масив:
function getMaximum(values: readonly number[]): number {
if (values.length === 0) {
throw new Error("Колекція не може бути порожньою");
}
return Math.max(...values);
}
const mutableValues = [4, 9, 2];
const readonlyValues: readonly number[] = [7, 3, 8];
console.log(getMaximum(mutableValues));
console.log(getMaximum(readonlyValues));Це зручний спосіб показати, що функція не змінює вхідні дані.
Водночас readonly-масив не можна передати у функцію, яка потенційно змінює масив:
function appendValue(values: number[], value: number): void {
values.push(value);
}
const values: readonly number[] = [1, 2, 3];
// appendValue(values, 4); // Помилка: readonly-масив не можна передати як number[]Якщо функції справді потрібно змінювати колекцію, можна створити змінну копію:
const values: readonly number[] = [1, 2, 3];
const mutableCopy = [...values];
appendValue(mutableCopy, 4);
console.log(mutableCopy);readonly захищає лише сам масив і лише під час перевірки TypeScript. Він не заморожує об’єкт у пам’яті під час виконання JavaScript.
type User = {
name: string;
};
const users: readonly User[] = [
{ name: "Олена" },
];
users[0].name = "Ірина";У цьому прикладі не можна замінити або видалити елемент масиву, але властивість об’єкта User залишається змінною.
Якщо потрібно заборонити зміну і властивостей елементів, їх також треба оголосити як readonly:
type User = {
readonly name: string;
};
const users: readonly User[] = [
{ name: "Олена" },
];
// users[0].name = "Ірина"; // Помилка компіляціїЦе все одно є захистом на рівні типів, а не автоматичним викликом Object.freeze.
Оператор spread ... розгортає елементи масиву в іншому місці:
const first = [1, 2, 3];
const second = [4, 5];
const combined = [...first, ...second];
console.log(combined);Spread створює новий масив. Початкові масиви не змінюються:
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3]
console.log(copy); // [1, 2, 3, 4]Readonly-масив можна розгорнути у новий масив:
const readonlyValues: readonly number[] = [10, 20, 30];
const mutableValues = [...readonlyValues];
mutableValues.push(40);
console.log(mutableValues);Тип mutableValues у цьому випадку — number[]. Сам факт використання spread створює нову змінну колекцію, але тип також можна зробити readonly:
const readonlyValues: readonly number[] = [10, 20, 30];
const anotherReadonlyValues: readonly number[] = [
...readonlyValues,
40,
];
// anotherReadonlyValues.push(50); // Помилка компіляціїМасив можна передати як окремі аргументи функції:
function sum(first: number, second: number, third: number): number {
return first + second + third;
}
const values: readonly [number, number, number] = [10, 20, 30];
console.log(sum(...values));Тут важливо, щоб кількість і типи елементів відповідали параметрам функції. Для цього зручно використовувати readonly-кортеж.
as constКортеж описує масив із фіксованою кількістю елементів і відомими позиційними типами:
const userRecord: [number, string] = [101, "Олена"];
userRecord[0] = 102;Readonly-кортеж не дозволяє змінювати його елементи:
const userRecord: readonly [number, string] = [101, "Олена"];
// userRecord[0] = 102; // Помилка компіляціїМодифікатор as const робить літеральне значення максимально вузьким і readonly:
const point = [10, 20] as const;
// Тип: readonly [10, 20]Такий кортеж можна безпечно розгорнути у функцію:
function move(x: number, y: number): string {
return `Точка: ${x}, ${y}`;
}
const point = [10, 20] as const;
console.log(move(...point));Оператор rest збирає довільну кількість аргументів у масив:
function sumAll(...values: number[]): number {
return values.reduce((total, value) => total + value, 0);
}
console.log(sumAll(1, 2, 3));
console.log(sumAll(10, 20, 30, 40));Параметр values усередині функції має тип number[].
Оскільки rest-параметр створює новий локальний масив, його зміни не змінюють масив, з якого були взяті аргументи:
function sortValues(...values: number[]): number[] {
return values.sort((a, b) => a - b);
}
const original: readonly number[] = [30, 10, 20];
const sorted = sortValues(...original);
console.log(original);
console.log(sorted);Для передачі масиву в rest-параметр використовується spread:
const values: readonly number[] = [2, 4, 6];
console.log(sumAll(...values));Readonly-масив можна розгорнути в ...values: number[], тому що функція отримує окремі значення і створює власний масив аргументів.
Rest-параметр може мати тип кортежу, якщо кількість і типи аргументів фіксовані:
function formatUser(...user: [number, string]): string {
const [id, name] = user;
return `${id}: ${name}`;
}
console.log(formatUser(101, "Олена"));У цьому випадку функція повинна отримати рівно два аргументи: число і рядок.
Для узагальнених функцій можна зберегти типи аргументів за допомогою параметра, що розширює readonly-масив:
function addPrefix<T extends readonly unknown[]>(
prefix: string,
...values: T
): [string, ...T] {
return [prefix, ...values];
}
const coordinates = [10, 20] as const;
const result = addPrefix("point", ...coordinates);
console.log(result);Тип result зберігає структуру аргументів:
// Тип result: [string, 10, 20]Такий підхід корисний, коли функція додає або комбінує значення, але потрібно не втратити типи й порядок елементів.
Розглянемо повний приклад функцій для обробки оцінок:
function average(values: readonly number[]): number {
if (values.length === 0) {
throw new Error("Неможливо обчислити середнє для порожньої колекції");
}
const total = values.reduce((sum, value) => sum + value, 0);
return total / values.length;
}
function addScores(
first: readonly number[],
...additional: number[]
): number[] {
return [...first, ...additional];
}
const initialScores = [80, 90, 75] as const;
const allScores = addScores(initialScores, 88, 95);
console.log(allScores);
console.log(average(allScores));Тут:
average приймає будь-який масив чисел, але не може його змінити;
initialScores є readonly-кортежем;
addScores отримує першу колекцію і довільну кількість додаткових значень;
spread створює новий масив allScores;
початковий initialScores залишається незмінним.
function clear(values: readonly string[]): void {
// values.length = 0; // Помилка компіляції
// values.pop(); // Помилка компіляції
}Якщо потрібна змінна копія, створіть її через spread:
function clear(values: readonly string[]): string[] {
const copy = [...values];
copy.length = 0;
return copy;
}const і readonlyconst забороняє переприсвоїти змінну, але не забороняє змінювати масив:
const values = [1, 2, 3];
values.push(4); // Дозволено
// values = [5, 6, 7]; // Помилка: переприсвоєння constДля заборони методів зміни потрібен readonly-тип:
const values: readonly number[] = [1, 2, 3];
// values.push(4); // Помилкаtype Product = {
name: string;
};
const products: readonly Product[] = [
{ name: "Книга" },
];
// products.push({ name: "Ручка" }); // Помилка
products[0].name = "Зошит"; // ДозволеноReadonly-масив не робить його об’єкти readonly. За потреби додайте readonly до властивостей типу елемента.
Spread копіює елементи, але не робить результат readonly автоматично:
const source: readonly number[] = [1, 2, 3];
const copy = [...source];
copy.push(4); // Дозволено, copy має тип number[]Якщо результат теж не повинен змінюватися, вкажіть тип явно:
const readonlyCopy: readonly number[] = [...source];
// readonlyCopy.push(4); // ПомилкаФункція з rest-параметром очікує окремі аргументи:
function multiplyAll(...values: number[]): number {
return values.reduce((result, value) => result * value, 1);
}
const values = [2, 3, 4];
console.log(multiplyAll(...values));Виклик multiplyAll(values) передав би один аргумент-масив і не відповідав би типу number[] для окремого елемента.
readonly T[] і ReadonlyArray<T> забороняють змінювальні операції через конкретне посилання.
Readonly-масиви особливо корисні в параметрах функцій, які лише читають дані.
readonly не робить об’єкти всередині масиву незмінними.
const захищає змінну від переприсвоєння, але не захищає сам масив від змін.
Spread ...values розгортає елементи масиву й створює нову колекцію або передає елементи як аргументи.
Rest ...values збирає аргументи функції у локальний масив.
as const створює readonly-кортеж із вузькими літеральними типами.
Для збереження типів кортежів у generic-функціях використовують T extends readonly unknown[].