Масиви, кортежі та об
Масиви, кортежі та об'єкти
🎯 Мета розділу
- Зрозуміти, як масиви зберігаються у пам'яті та чому вони передаються за посиланням.
- Навчитися типізувати масиви, кортежі та об'єкти у TypeScript.
- Освоїти inline-синтаксис об'єктних типів без використання псевдонімів.
- Вивчити модифікатор
readonlyдля захисту даних від змін. - Опанувати індексні сигнатури для динамічних ключів.
- Усвідомити різницю між масивами та кортежами.
🔑 Ключові терміни
- Reference Type: тип даних, що зберігається за посиланням (масиви, об'єкти, функції).
- Array: впорядкована колекція елементів однакового або різних типів.
- Tuple: масив фіксованої довжини з типізацією кожної позиції.
- Object Type: структура даних з іменованими властивостями.
- Index Signature: синтаксис для опису об'єктів з динамічними ключами.
- Readonly: модифікатор, що забороняє зміну значення після ініціалізації.
Масиви: впорядковані колекції даних
Що таке масив у JavaScript
Масив (array) — це спеціальний об'єкт, що зберігає впорядковану колекцію елементів. На відміну від примітивних типів, масив є reference type — зберігається у купі (heap), а змінна містить лише посилання на нього.
// Створення масиву
let numbers = [1, 2, 3, 4, 5];
let fruits = ["apple", "banana", "orange"];
let mixed = [42, "text", true, null]; // різні типи!
// Доступ до елементів (індексація з 0)
console.log(numbers[0]); // 1
console.log(fruits[2]); // "orange"
// Зміна елементів
numbers[0] = 100;
console.log(numbers); // [100, 2, 3, 4, 5]
Масиви зберігаються за посиланням
Критично важливо розуміти: коли ви присвоюєте масив іншій змінній, копіюється посилання, а не сам масив:
let original = [1, 2, 3];
let copy = original; // copy отримує ПОСИЛАННЯ на той самий масив!
copy[0] = 999; // змінюємо через copy
console.log(original); // [999, 2, 3] — оригінал теж змінився!
console.log(copy); // [999, 2, 3]
console.log(original === copy); // true (обидві змінні вказують на один масив)
Як зробити справжню копію масиву:
let original = [1, 2, 3];
// Спосіб 1: Spread оператор (найпопулярніший)
let copy1 = [...original];
// Спосіб 2: Array.from()
let copy2 = Array.from(original);
// Спосіб 3: slice() без параметрів
let copy3 = original.slice();
// Тепер зміни не впливають на оригінал
copy1[0] = 999;
console.log(original); // [1, 2, 3] — не змінився ✓
console.log(copy1); // [999, 2, 3]
let users = [
{ name: "Alice", age: 28 },
{ name: "Bob", age: 32 }
];
let copy = [...users]; // shallow copy
copy[0].age = 100; // змінюємо об'єкт через копію
console.log(users[0].age); // 100 — оригінальний об'єкт теж змінився!
structuredClone() у сучасних браузерах або бібліотека lodash.cloneDeep).Типізація масивів у TypeScript
TypeScript дозволяє вказати тип елементів масиву двома способами:
Спосіб 1: T[] (рекомендований)
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];
let flags: boolean[] = [true, false, true];
// Вкладені масиви (двовимірні)
let matrix: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// Масиви масивів рядків
let groups: string[][] = [
["Alice", "Bob"],
["Charlie", "Dave"]
];
Спосіб 2: Array<T> (generic-синтаксис)
let numbers: Array<number> = [1, 2, 3];
let names: Array<string> = ["Alice", "Bob"];
Обидва способи функціонально ідентичні. T[] є коротшим і більш читабельним, тому використовується частіше.
TypeScript перевіряє типи елементів:
let numbers: number[] = [1, 2, 3];
numbers.push(4); // OK: число
numbers.push("five"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'
numbers[0] = 100; // OK: число
numbers[1] = true; // Error: Type 'boolean' is not assignable to type 'number'
Порожній масив та type inference
Коли ви створюєте порожній масив без анотації, TypeScript виводить тип any[], що небезпечно:
// ❌ Небезпечно: TypeScript виводить any[]
let items = [];
items.push(1); // OK
items.push("text"); // OK
items.push(true); // OK — масив стає [number, string, boolean]
Правильно: завжди вказуйте тип для порожніх масивів:
// ✅ Безпечно: явна типізація
let items: number[] = [];
items.push(1); // OK
items.push("text"); // Error: тип string не підходить
Методи роботи з масивами
JavaScript надає багато методів для роботи з масивами. Деякі змінюють оригінальний масив (mutating), інші — повертають новий (non-mutating).
Методи, що змінюють масив:
let numbers: number[] = [1, 2, 3];
// push: додає елемент в кінець
numbers.push(4); // [1, 2, 3, 4]
// pop: видаляє останній елемент
let last: number | undefined = numbers.pop(); // 4, масив: [1, 2, 3]
// unshift: додає елемент на початок
numbers.unshift(0); // [0, 1, 2, 3]
// shift: видаляє перший елемент
let first: number | undefined = numbers.shift(); // 0, масив: [1, 2, 3]
// splice: видаляє/додає елементи в середині
numbers.splice(1, 1); // видаляє 1 елемент з індексу 1, масив: [1, 3]
// reverse: перевертає масив
numbers.reverse(); // [3, 1]
// sort: сортує масив
numbers.sort(); // [1, 3]
Методи, що повертають новий масив:
let numbers: number[] = [1, 2, 3, 4, 5];
// map: перетворює кожен елемент
let doubled: number[] = numbers.map((n) => n * 2); // [2, 4, 6, 8, 10]
// filter: залишає лише елементи, що пройшли умову
let evens: number[] = numbers.filter((n) => n % 2 === 0); // [2, 4]
// slice: копіює частину масиву
let part: number[] = numbers.slice(1, 3); // [2, 3] (індекси 1, 2)
// concat: об'єднує масиви
let more: number[] = numbers.concat([6, 7, 8]); // [1, 2, 3, 4, 5, 6, 7, 8]
// flat: розгортає вкладені масиви
let nested: number[][] = [[1, 2], [3, 4]];
let flattened: number[] = nested.flat(); // [1, 2, 3, 4]
console.log(numbers); // [1, 2, 3, 4, 5] — оригінал не змінився!
Методи пошуку та перевірки:
let numbers: number[] = [1, 2, 3, 4, 5];
// includes: чи є елемент у масиві
let hasThree: boolean = numbers.includes(3); // true
// indexOf: індекс першого входження (-1 якщо не знайдено)
let index: number = numbers.indexOf(3); // 2
// find: повертає перший елемент, що пройшов умову
let found: number | undefined = numbers.find((n) => n > 3); // 4
// findIndex: індекс першого елемента, що пройшов умову
let foundIndex: number = numbers.findIndex((n) => n > 3); // 3 (індекс числа 4)
// some: чи є хоча б один елемент, що пройшов умову
let hasEven: boolean = numbers.some((n) => n % 2 === 0); // true
// every: чи всі елементи пройшли умову
let allPositive: boolean = numbers.every((n) => n > 0); // true
map, filter, reduce, find, some, every не змінюють оригінальний масив, тому їх називають pure functions (чисті функції). Це робить код більш передбачуваним та безпечним, особливо у функціональному стилі програмування.Багатовимірні масиви
TypeScript підтримує масиви будь-якої розмірності. Найчастіше використовуються двовимірні масиви (матриці):
// Двовимірний масив (матриця 3x3)
let matrix: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// Доступ до елементів
console.log(matrix[0][0]); // 1 (перший рядок, перша колонка)
console.log(matrix[1][2]); // 6 (другий рядок, третя колонка)
// Ітерація по матриці
for (let row of matrix) {
for (let cell of row) {
console.log(cell);
}
}
// Або через forEach
matrix.forEach((row, i) => {
row.forEach((cell, j) => {
console.log(`[${i}][${j}] = ${cell}`);
});
});
Тривимірні та багатовимірні масиви:
// Тривимірний масив (куб 2x2x2)
let cube: number[][][] = [
[
[1, 2],
[3, 4]
],
[
[5, 6],
[7, 8]
]
];
console.log(cube[0][1][0]); // 3
// Масив з різними рівнями вкладеності
let jagged: number[][] = [
[1, 2],
[3, 4, 5], // рядки можуть мати різну довжину!
[6]
];
Readonly масиви: захист від змін
Чому потрібна незмінність
У багатьох випадках ви хочете передати масив функції, але гарантувати, що функція не зміниь його вміст. Або створити константний масив даних, який не має змінюватися протягом виконання програми.
const config = ["/api/v1", "/api/v2", "/health"];
// Хтось може випадково змінити масив:
config.push("/admin"); // Ой! Тепер config змінений
config[0] = "/legacy"; // Ой! Перший елемент перезаписаний
Хоча ми використали const, це захищає лише посилання на масив від переприсвоєння, але НЕ сам масив від змін:
const numbers = [1, 2, 3];
numbers = [4, 5, 6]; // Error: Cannot assign to 'numbers' because it is a constant
numbers.push(4); // OK: сам масив можна змінювати!
Синтаксис readonly масивів
TypeScript надає модифікатор readonly для масивів:
// Спосіб 1: readonly T[]
let numbers: readonly number[] = [1, 2, 3, 4, 5];
// Спосіб 2: ReadonlyArray<T> (generic-синтаксис)
let names: ReadonlyArray<string> = ["Alice", "Bob", "Charlie"];
Що забороняє readonly:
let numbers: readonly number[] = [1, 2, 3];
// ❌ Заборонені всі методи, що змінюють масив:
numbers.push(4); // Error: Property 'push' does not exist on type 'readonly number[]'
numbers.pop(); // Error
numbers.shift(); // Error
numbers.unshift(0); // Error
numbers.splice(1, 1); // Error
numbers.reverse(); // Error
numbers.sort(); // Error
numbers[0] = 999; // Error: Index signature in type 'readonly number[]' only permits reading
// ✅ Дозволені методи, що НЕ змінюють масив:
let doubled = numbers.map(n => n * 2); // OK: map повертає новий масив
let evens = numbers.filter(n => n % 2 === 0); // OK
let sum = numbers.reduce((acc, n) => acc + n, 0); // OK
console.log(numbers[0]); // OK: читання дозволено
Практичний приклад: захист конфігурації
// Конфігурація API endpoints (не має змінюватися)
const API_ENDPOINTS: readonly string[] = [
"/api/v1/users",
"/api/v1/posts",
"/api/v1/comments"
];
function makeRequest(endpoint: string): void {
if (!API_ENDPOINTS.includes(endpoint)) {
throw new Error(`Unknown endpoint: ${endpoint}`);
}
console.log(`Making request to ${endpoint}`);
}
// Ніхто не може випадково змінити конфігурацію:
// API_ENDPOINTS.push("/api/v1/admin"); // Error!
Readonly та вкладені структури
Важливо розуміти: readonly робить поверхневу незмінність (shallow immutability):
let users: readonly { name: string; age: number }[] = [
{ name: "Alice", age: 28 },
{ name: "Bob", age: 32 }
];
// ❌ Не можна додавати/видаляти елементи масиву:
// users.push({ name: "Charlie", age: 25 }); // Error
// ✅ Але можна змінювати властивості об'єктів всередині масиву!
users[0].age = 100; // OK — об'єкт не readonly!
console.log(users[0]); // { name: "Alice", age: 100 }
Рішення: зробити об'єкти теж readonly:
let users: readonly (readonly { name: string; age: number })[] = [
{ name: "Alice", age: 28 },
{ name: "Bob", age: 32 }
];
// Тепер нічого не можна змінювати:
// users[0].age = 100; // Error: Cannot assign to 'age' because it is a read-only property
Але це виглядає громіздко. Пізніше, коли ви вивчите interface та utility types, буде простіше писати складні readonly структури.
Кортежі: масиви з фіксованою структурою
Що таке кортеж (tuple)
Кортеж (tuple) — це масив фіксованої довжини, де кожна позиція має власний тип. На відміну від звичайного масиву (number[]), де всі елементи мають однаковий тип, кортеж дозволяє змішувати різні типи у чіткій послідовності.
Приклад із реального життя:
// Координати на карті: [широта, довгота]
let coordinates: [number, number] = [50.4501, 30.5234];
// Користувач: [ім'я, вік, активний]
let user: [string, number, boolean] = ["Alice", 28, true];
// HTTP відповідь: [статус-код, тіло відповіді]
let response: [number, string] = [200, "OK"];
Типізація кортежів
Синтаксис кортежу — квадратні дужки з типами через кому:
// Кортеж з 2 елементів: рядок та число
let pair: [string, number] = ["age", 25];
// Кортеж з 3 елементів: число, рядок, boolean
let triple: [number, string, boolean] = [1, "hello", true];
// Кортеж з вкладеним кортежем
let nested: [string, [number, number]] = ["point", [10, 20]];
TypeScript строго перевіряє типи та кількість елементів:
let user: [string, number] = ["Alice", 28];
user = ["Bob", 32]; // OK: типи співпадають
user = ["Charlie", 25, true]; // Error: Source has 3 element(s) but target allows only 2
user = [28, "Alice"]; // Error: порядок типів не співпадає
user = ["David"]; // Error: Source has 1 element(s) but target requires 2
Доступ до елементів кортежу
Доступ до елементів кортежу виглядає так само, як до масиву, але TypeScript знає тип кожної позиції:
let user: [string, number, boolean] = ["Alice", 28, true];
let name: string = user[0]; // OK: TypeScript знає, що user[0] — це string
let age: number = user[1]; // OK: TypeScript знає, що user[1] — це number
let active: boolean = user[2]; // OK: TypeScript знає, що user[2] — це boolean
// TypeScript допомагає уникнути помилок:
let wrong: number = user[0]; // Error: Type 'string' is not assignable to type 'number'
Вихід за межі кортежу:
let pair: [string, number] = ["age", 25];
console.log(pair[0]); // "age"
console.log(pair[1]); // 25
console.log(pair[2]); // undefined (виконання), але TypeScript покаже помилку!
// Error: Tuple type '[string, number]' of length '2' has no element at index '2'
Деструктуризація кортежів
Кортежі ідеально підходять для деструктуризації, бо TypeScript знає типи кожної позиції:
let user: [string, number, boolean] = ["Alice", 28, true];
// Деструктуризація з типізацією
let [name, age, active] = user;
console.log(name); // "Alice" (тип string)
console.log(age); // 28 (тип number)
console.log(active); // true (тип boolean)
// TypeScript автоматично виводить типи змінних!
// name: string
// age: number
// active: boolean
Практичний приклад: функція, що повертає кортеж:
function getUser(id: number): [string, number] {
// У реальності тут був би запит до бази даних
return ["Alice", 28];
}
let [userName, userAge] = getUser(1);
console.log(userName); // "Alice" (тип string)
console.log(userAge); // 28 (тип number)
Опціональні елементи у кортежах
Ви можете позначити елементи кортежу як опціональні за допомогою ?:
// Третій елемент опціональний
let user: [string, number, boolean?] = ["Alice", 28];
// Обидва варіанти валідні:
let user1: [string, number, boolean?] = ["Bob", 32];
let user2: [string, number, boolean?] = ["Charlie", 25, true];
// TypeScript знає, що третій елемент може бути undefined:
let [name, age, active] = user;
console.log(active); // undefined (тип boolean | undefined)
Rest елементи у кортежах
Кортежі підтримують rest-синтаксис для змінної кількості елементів:
// Перші два елементи фіксовані, решта — числа
let data: [string, boolean, ...number[]] = ["test", true, 1, 2, 3, 4, 5];
// Мінімум 2 елементи (string та boolean), потім будь-яка кількість чисел
let minimal: [string, boolean, ...number[]] = ["test", true]; // OK
let withNumbers: [string, boolean, ...number[]] = ["test", false, 10, 20]; // OK
Readonly кортежі
Так само, як масиви, кортежі можна зробити незмінними:
let coordinates: readonly [number, number] = [50.4501, 30.5234];
console.log(coordinates[0]); // OK: читання дозволено
// ❌ Зміна заборонена:
// coordinates[0] = 51.0; // Error: Cannot assign to '0' because it is a read-only property
// coordinates.push(40.0); // Error: Property 'push' does not exist on type 'readonly [number, number]'
Кортежі vs масиви: коли що використовувати
Використовуйте кортежі, коли:
- Кількість елементів фіксована і завжди однакова
- Кожна позиція має специфічне значення (широта/довгота, ім'я/вік)
- Ви працюєте з парами, трійками чи іншими невеликими структурами
- Функція повертає кілька значень різних типів
Використовуйте масиви, коли:
- Кількість елементів може змінюватися динамічно
- Всі елементи мають однаковий тип та значення
- Ви ітеруєте по колекції однотипних даних
- Розмір колекції заздалегідь невідомий
// ✅ Кортеж: фіксована структура "RGB колір"
let color: [number, number, number] = [255, 128, 0];
// ✅ Масив: динамічна колекція оцінок студента
let grades: number[] = [85, 90, 78, 92];
grades.push(88); // можемо додавати нові оцінки
// ✅ Кортеж: функція повертає результат та помилку
function divide(a: number, b: number): [number | null, string | null] {
if (b === 0) {
return [null, "Division by zero"];
}
return [a / b, null];
}
let [result, error] = divide(10, 2);
if (error) {
console.error(error);
} else {
console.log(result);
}
Tuple або Pair в інших мовах). Натомість використовуйте кортежі: [string, number] для пари, [string, number, boolean] для трійки тощо.Об'єкти: структури з іменованими властивостями
Що таке об'єкт у JavaScript
Об'єкт (object) — це колекція іменованих властивостей (пар ключ-значення). На відміну від масивів, де доступ до елементів здійснюється через числовий індекс, у об'єктах доступ — через ключ-рядок або Symbol.
// Створення об'єкта
let user = {
name: "Alice",
age: 28,
email: "alice@example.com",
isActive: true
};
// Доступ до властивостей
console.log(user.name); // "Alice" (dot notation)
console.log(user["age"]); // 28 (bracket notation)
// Зміна властивостей
user.age = 29;
user["email"] = "alice.new@example.com";
// Додавання нових властивостей
user.city = "Kyiv";
console.log(user);
// {
// name: "Alice",
// age: 29,
// email: "alice.new@example.com",
// isActive: true,
// city: "Kyiv"
// }
Об'єкти зберігаються за посиланням
Так само, як масиви, об'єкти є reference type:
let original = { name: "Alice", age: 28 };
let copy = original; // копіюється ПОСИЛАННЯ
copy.age = 100;
console.log(original.age); // 100 — оригінал теж змінився!
console.log(original === copy); // true (обидва вказують на один об'єкт)
Створення справжньої копії об'єкта:
let original = { name: "Alice", age: 28 };
// Спосіб 1: Spread оператор (shallow copy)
let copy1 = { ...original };
// Спосіб 2: Object.assign (shallow copy)
let copy2 = Object.assign({}, original);
copy1.age = 100;
console.log(original.age); // 28 — не змінився ✓
Object.assign роблять поверхневу копію (shallow copy). Якщо об'єкт містить вкладені об'єкти, вони копіюються за посиланням:let user = {
name: "Alice",
address: {
city: "Kyiv",
country: "Ukraine"
}
};
let copy = { ...user };
copy.address.city = "Lviv"; // змінюємо вкладений об'єкт
console.log(user.address.city); // "Lviv" — оригінал теж змінився!
structuredClone() або бібліотеки на кшталт lodash.cloneDeep.Типізація об'єктів у TypeScript: inline-синтаксис
У TypeScript ви можете типізувати об'єкт безпосередньо в анотації типу через inline об'єктний тип:
// Inline об'єктний тип
let user: { name: string; age: number; email: string } = {
name: "Alice",
age: 28,
email: "alice@example.com"
};
// Функція з параметром-об'єктом
function greetUser(user: { name: string; age: number }): string {
return `Hello, ${user.name}! You are ${user.age} years old.`;
}
console.log(greetUser({ name: "Bob", age: 32 }));
// "Hello, Bob! You are 32 years old."
Синтаксис inline типу:
- Фігурні дужки
{ } - Імена властивостей з типами:
name: string - Розділювач: крапка з комою
;або кома,(обидва варіанти валідні)
// Обидва синтаксиси однакові:
let user1: { name: string; age: number } = { name: "Alice", age: 28 };
let user2: { name: string, age: number } = { name: "Bob", age: 32 };
// Рекомендується використовувати крапку з комою (;) для консистентності
TypeScript перевіряє структуру об'єкта
TypeScript строго перевіряє, що об'єкт має всі обов'язкові властивості та не має зайвих:
let user: { name: string; age: number } = {
name: "Alice",
age: 28
}; // OK
// ❌ Помилка: відсутня властивість age
let incomplete: { name: string; age: number } = {
name: "Bob"
};
// Error: Property 'age' is missing in type '{ name: string; }' but required in type '{ name: string; age: number; }'
// ❌ Помилка: зайва властивість email
let extra: { name: string; age: number } = {
name: "Charlie",
age: 25,
email: "charlie@example.com"
};
// Error: Object literal may only specify known properties, and 'email' does not exist in type '{ name: string; age: number; }'
function greet(user: { name: string }): void {
console.log(`Hello, ${user.name}!`);
}
// ❌ Помилка: зайва властивість
greet({ name: "Alice", age: 28 });
// Error: Object literal may only specify known properties
// ✅ OK: об'єкт створений окремо
let user = { name: "Bob", age: 32 };
greet(user); // OK! TypeScript перевіряє лише наявність name
Вкладені об'єктні типи
Об'єкти можуть містити інші об'єкти:
let user: {
name: string;
age: number;
address: {
city: string;
country: string;
};
} = {
name: "Alice",
age: 28,
address: {
city: "Kyiv",
country: "Ukraine"
}
};
// Доступ до вкладених властивостей
console.log(user.address.city); // "Kyiv"
// Зміна вкладених властивостей
user.address.city = "Lviv";
Масиви об'єктів:
let users: { name: string; age: number }[] = [
{ name: "Alice", age: 28 },
{ name: "Bob", age: 32 },
{ name: "Charlie", age: 25 }
];
// Доступ до елементів
console.log(users[0].name); // "Alice"
// Ітерація
users.forEach(user => {
console.log(`${user.name} is ${user.age} years old`);
});
// Фільтрація
let adults = users.filter(user => user.age >= 30);
console.log(adults); // [{ name: "Bob", age: 32 }]
Опціональні властивості
Якщо властивість може бути відсутня, позначте її через ?:
let user: {
name: string;
age: number;
email?: string; // опціональна властивість
} = {
name: "Alice",
age: 28
// email відсутній — це OK!
};
// TypeScript знає, що email може бути undefined
let email: string | undefined = user.email;
if (user.email) {
console.log(`Email: ${user.email}`);
} else {
console.log("Email not provided");
}
Опціональна властивість vs undefined:
// Опціональна властивість: може бути відсутня
let user1: { name: string; email?: string } = { name: "Alice" };
// Явно undefined: властивість ОБОВ'ЯЗКОВА, але може бути undefined
let user2: { name: string; email: string | undefined } = { name: "Bob", email: undefined };
// user1 може не мати email взагалі
// user2 ПОВИНЕН мати email, навіть якщо це undefined
У більшості випадків email?: string зручніше, ніж email: string | undefined.
Readonly властивості
Щоб заборонити зміну властивості після створення об'єкта, використовуйте readonly:
let user: {
readonly id: number;
name: string;
age: number;
} = {
id: 1,
name: "Alice",
age: 28
};
// ✅ Читання дозволено
console.log(user.id); // 1
// ✅ Зміна звичайних властивостей дозволена
user.name = "Alicia";
user.age = 29;
// ❌ Зміна readonly властивості заборонена
// user.id = 2;
// Error: Cannot assign to 'id' because it is a read-only property
Використання readonly для захисту критичних даних:
function processUser(user: {
readonly id: number;
name: string;
}): void {
// Функція не може випадково змінити ID
// user.id = 999; // Error!
// Але може змінити name
user.name = user.name.toUpperCase();
}
Readonly та вкладені об'єкти:
Так само, як у масивів, readonly працює поверхнево:
let user: {
readonly id: number;
address: {
city: string;
};
} = {
id: 1,
address: { city: "Kyiv" }
};
// ❌ Не можна змінити id
// user.id = 2; // Error
// ✅ Але можна змінити вкладений об'єкт!
user.address.city = "Lviv"; // OK
Щоб зробити вкладений об'єкт теж readonly, додайте модифікатор явно:
let user: {
readonly id: number;
readonly address: {
readonly city: string;
};
} = {
id: 1,
address: { city: "Kyiv" }
};
// Тепер нічого не можна змінювати:
// user.address.city = "Lviv"; // Error: Cannot assign to 'city'
Index signatures: динамічні ключі
Проблема: невідомі ключі
Іноді структура об'єкта заздалегідь невідома — кількість та імена властивостей визначаються динамічно:
// Словник перекладів: ключ — мова, значення — текст
let translations = {
en: "Hello",
uk: "Привіт",
de: "Hallo",
fr: "Bonjour"
};
// Як типізувати такий об'єкт, якщо ми не знаємо всі можливі мови?
Синтаксис index signature
Index signature дозволяє описати об'єкт з невідомими ключами:
// Будь-який ключ (string) → значення (string)
let translations: { [key: string]: string } = {
en: "Hello",
uk: "Привіт",
de: "Hallo",
fr: "Bonjour"
};
// Доступ до властивостей
console.log(translations["en"]); // "Hello"
console.log(translations["uk"]); // "Привіт"
// Додавання нових властивостей
translations["es"] = "Hola";
translations["it"] = "Ciao";
Синтаксис:
{ [key: КлючТип]: ЗначенняТип }
key— довільна назва змінної (можна назватиk,prop,idтощо)КлючТип— майже завждиstringабоnumberЗначенняТип— тип значень властивостей
Приклади index signatures
Словник з числовими значеннями:
// Оцінки студентів: ім'я → оцінка
let grades: { [studentName: string]: number } = {
Alice: 95,
Bob: 87,
Charlie: 92
};
grades["David"] = 88; // OK
// grades["Eve"] = "A+"; // Error: Type 'string' is not assignable to type 'number'
Конфігурація з boolean значеннями:
// Налаштування фіч: назва фічі → увімкнена/вимкнена
let features: { [feature: string]: boolean } = {
darkMode: true,
notifications: false,
analytics: true
};
console.log(features["darkMode"]); // true
features["betaFeatures"] = false; // OK
Числові ключі (масивоподібні об'єкти):
// Об'єкт з числовими індексами
let items: { [index: number]: string } = {
0: "apple",
1: "banana",
2: "orange"
};
console.log(items[0]); // "apple"
console.log(items[1]); // "banana"
items[3] = "grape"; // OK
number як тип ключа, у JavaScript всі ключі об'єктів насправді є рядками. Коли ви пишете obj[0], JavaScript перетворює 0 на "0". TypeScript знає про це і дозволяє обидва варіанти доступу.Index signature з додатковими властивостями
Ви можете комбінувати index signature з явними властивостями:
let config: {
appName: string; // обов'язкова властивість
version: string; // обов'язкова властивість
[key: string]: string; // інші довільні властивості
} = {
appName: "MyApp",
version: "1.0.0",
author: "Alice",
license: "MIT"
};
console.log(config.appName); // "MyApp"
console.log(config.author); // "Alice"
config.theme = "dark"; // OK: додаємо нову властивість
Обмеження: Всі явні властивості мають відповідати типу значення index signature:
// ❌ Помилка: version має тип number, але index signature вимагає string
let config: {
appName: string;
version: number; // Error!
[key: string]: string;
} = {
appName: "MyApp",
version: 1
};
// Error: Property 'version' of type 'number' is not assignable to 'string' index type 'string'
Рішення: Використовуйте union type для index signature:
let config: {
appName: string;
version: number;
[key: string]: string | number; // ключі можуть бути string або number
} = {
appName: "MyApp",
version: 1,
maxUsers: 100,
author: "Alice"
};
Index signature з readonly
Щоб заборонити зміну властивостей, додайте readonly:
let constants: {
readonly [key: string]: number;
} = {
PI: 3.14159,
E: 2.71828,
GOLDEN_RATIO: 1.61803
};
console.log(constants.PI); // 3.14159
// ❌ Зміна заборонена
// constants.PI = 3.14; // Error: Index signature in type '...' only permits reading
// constants.SPEED_OF_LIGHT = 299792458; // Error
Коли використовувати index signatures
Використовуйте index signatures, коли:
- Ключі об'єкта невідомі заздалегідь (словники, мапи, конфігурації)
- Кількість властивостей динамічна
- Ви працюєте з даними зовнішніх джерел (JSON API, файли конфігурації)
НЕ використовуйте index signatures, коли:
- Структура об'єкта фіксована і відома (краще явно перелічити властивості)
- Потрібна строга типізація кожної властивості
- Ви хочете автодоповнення в IDE (index signatures не дають підказок для імен ключів)
// ❌ Погано: index signature для фіксованої структури
let user: { [key: string]: string | number } = {
name: "Alice",
age: 28,
email: "alice@example.com"
};
// Немає автодоповнення, можна додати будь-які ключі
// ✅ Добре: явна типізація
let user: { name: string; age: number; email: string } = {
name: "Alice",
age: 28,
email: "alice@example.com"
};
// IDE підказує властивості, TypeScript перевіряє їх наявність
Методи об'єктів та this
Об'єкти можуть містити функції
Функція, що є властивістю об'єкта, називається методом:
let user: {
name: string;
age: number;
greet: () => string; // метод без параметрів, повертає string
} = {
name: "Alice",
age: 28,
greet: () => {
return "Hello!";
}
};
console.log(user.greet()); // "Hello!"
Метод з параметрами:
let calculator: {
add: (a: number, b: number) => number;
subtract: (a: number, b: number) => number;
} = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
};
console.log(calculator.add(5, 3)); // 8
console.log(calculator.subtract(10, 4)); // 6
Альтернативний синтаксис для методів
Замість стрілочних функцій можна використовувати method shorthand:
let user: {
name: string;
greet(): string; // метод через shorthand синтаксис
} = {
name: "Alice",
greet() { // function shorthand
return `Hello, I'm ${this.name}!`;
}
};
console.log(user.greet()); // "Hello, I'm Alice!"
Різниця між стрілочною функцією та method shorthand:
- Стрілочна функція
() =>: не має власногоthis, успадковуєthisз контексту - Method shorthand
method() {}: має власнийthis, що вказує на об'єкт
let user = {
name: "Alice",
// Method shorthand: this = user
greet1() {
return `Hello, I'm ${this.name}!`;
},
// Arrow function: this = undefined (або window у браузері)
greet2: () => {
// Error: 'this' implicitly has type 'any'
return `Hello, I'm ${this.name}!`;
}
};
console.log(user.greet1()); // "Hello, I'm Alice!"
console.log(user.greet2()); // Error або undefined
strict: true компілятор заборонить використовувати this у стрілочній функції без явної типізації контексту. Це захищає вас від помилок з неправильним this.Типізація this у методах
Для методів, що використовують this, TypeScript автоматично виводить тип:
let user: {
name: string;
age: number;
greet(): string;
haveBirthday(): void;
} = {
name: "Alice",
age: 28,
greet() {
// TypeScript знає, що this — це об'єкт з name: string
return `Hello, I'm ${this.name}!`;
},
haveBirthday() {
// TypeScript знає, що this.age — це number
this.age++;
}
};
console.log(user.age); // 28
user.haveBirthday();
console.log(user.age); // 29
interface або class, які ви вивчите у наступних розділах. Inline-типізація стає незручною для великих структур.Практичні приклади та патерни
Приклад 1: Валідація форми
function validateForm(form: {
username: string;
email: string;
age: number;
}): { valid: boolean; errors: string[] } {
let errors: string[] = [];
if (form.username.length < 3) {
errors.push("Username must be at least 3 characters");
}
if (!form.email.includes("@")) {
errors.push("Invalid email format");
}
if (form.age < 18) {
errors.push("Must be at least 18 years old");
}
return {
valid: errors.length === 0,
errors: errors
};
}
let form = {
username: "al",
email: "alice.example.com",
age: 16
};
let result = validateForm(form);
console.log(result);
// {
// valid: false,
// errors: [
// "Username must be at least 3 characters",
// "Invalid email format",
// "Must be at least 18 years old"
// ]
// }
Приклад 2: Робота зі словником
function countWords(text: string): { [word: string]: number } {
let counts: { [word: string]: number } = {};
let words = text.toLowerCase().split(/\s+/);
for (let word of words) {
if (counts[word]) {
counts[word]++;
} else {
counts[word] = 1;
}
}
return counts;
}
let text = "hello world hello TypeScript world";
let wordCounts = countWords(text);
console.log(wordCounts);
// {
// hello: 2,
// world: 2,
// typescript: 1
// }
Приклад 3: Конфігурація з readonly
function createConfig(): {
readonly apiUrl: string;
readonly timeout: number;
readonly retries: number;
} {
return {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3
};
}
let config = createConfig();
console.log(config.apiUrl); // "https://api.example.com"
// ❌ Не можна змінювати конфігурацію
// config.timeout = 10000; // Error: Cannot assign to 'timeout'
Приклад 4: Об'єкт з опціональними метаданими
function fetchUser(id: number): {
id: number;
name: string;
email: string;
avatar?: string; // опціонально
bio?: string; // опціонально
} {
// Імітація запиту до API
if (id === 1) {
return {
id: 1,
name: "Alice",
email: "alice@example.com",
avatar: "/avatars/alice.jpg"
// bio відсутнє
};
} else {
return {
id: 2,
name: "Bob",
email: "bob@example.com"
// avatar і bio відсутні
};
}
}
let user1 = fetchUser(1);
let user2 = fetchUser(2);
console.log(user1.avatar); // "/avatars/alice.jpg"
console.log(user2.avatar); // undefined
if (user2.avatar) {
console.log(`Avatar: ${user2.avatar}`);
} else {
console.log("No avatar");
}
Підсумок
У цьому розділі ви опанували складні типи даних TypeScript та їх внутрішню будову у JavaScript:
Масиви:
- Масиви — це reference type, що зберігаються у купі за посиланням.
- Типізація через
T[]синтаксис (наприклад,number[],string[][]). - Методи масивів: mutating (
push,pop,splice) та non-mutating (map,filter,slice). - Модифікатор
readonlyзахищає масив від змін. - Поверхнева vs глибока копія через spread оператор.
Кортежі:
- Кортежі — масиви фіксованої довжини з типізацією кожної позиції:
[string, number]. - Ідеально підходять для пар, трійок та функцій з кількома поверненими значеннями.
- Підтримують опціональні елементи (
[string, number?]) та rest-синтаксис. - Модифікатор
readonlyробить кортеж незмінним.
Об'єкти:
- Об'єкти — колекції іменованих властивостей, також reference type.
- Inline-типізація:
{ name: string; age: number }. - Опціональні властивості через
?:email?: string. - Модифікатор
readonlyдля захисту критичних властивостей. - Index signatures для динамічних ключів:
{ [key: string]: ValueType }. - Методи об'єктів та різниця між arrow functions та method shorthand.
Ключові принципи:
- Завжди типізуйте порожні масиви явно:
let items: number[] = []. - Використовуйте
readonlyдля захисту незмінних даних. - Inline-типізація зручна для простих структур, але для складних об'єктів краще використовувати
interface(ви вивчите їх пізніше). - Структурна типізація TypeScript дозволяє об'єктам мати «зайві» властивості у деяких контекстах.
Ключове слово const захищає лише посилання на масив від переприсвоєння, але не сам масив від модифікації:
const numbers = [1, 2, 3];
numbers = [4, 5, 6]; // Error: Cannot assign to 'numbers'
numbers.push(4); // OK: сам масив можна змінювати
Щоб заборонити зміни вмісту масиву, використовуйте readonly:
const numbers: readonly number[] = [1, 2, 3];
numbers.push(4); // Error: Property 'push' does not exist
Найбільший захист: const + readonly = незмінне посилання + незмінний вміст.
Масив (number[]) — це колекція елементів одного типу з динамічною довжиною:
let scores: number[] = [85, 90, 78];
scores.push(92); // можна додавати елементи
Кортеж ([string, number]) — це масив фіксованої довжини, де кожна позиція має свій тип:
let user: [string, number] = ["Alice", 28];
// user.push(true); // Error: не можна додавати елементи
Використовуйте кортежі для фіксованих структур (координати, пари ключ-значення), масиви — для динамічних колекцій.
Shallow copy (поверхнева копія) — копіюються лише властивості першого рівня. Вкладені об'єкти/масиви залишаються посиланнями:
let original = {
name: "Alice",
address: { city: "Kyiv" }
};
let copy = { ...original }; // shallow copy
copy.address.city = "Lviv";
console.log(original.address.city); // "Lviv" — оригінал теж змінився!
Deep copy (глибока копія) — рекурсивно копіюються всі вкладені структури:
let copy = structuredClone(original); // deep copy
copy.address.city = "Lviv";
console.log(original.address.city); // "Kyiv" — оригінал НЕ змінився ✓
Spread оператор { ...obj } та Object.assign() роблять лише shallow copy.
Використовуйте index signature, коли:
- Ключі об'єкта невідомі заздалегідь (словники, конфігурації, мапи).
- Кількість властивостей динамічна.
- Ви працюєте з даними зовнішніх джерел (JSON, API).
Приклад:
// Словник перекладів
let translations: { [key: string]: string } = {
en: "Hello",
uk: "Привіт"
};
translations["de"] = "Hallo"; // можна додавати нові ключі
НЕ використовуйте для фіксованих структур — краще явно перелічити властивості для автодоповнення в IDE.
Так, readonly — це лише перевірка на рівні TypeScript. Після компіляції у JavaScript код стає звичайним об'єктом без захисту:
let user: { readonly id: number; name: string } = { id: 1, name: "Alice" };
// TypeScript заборонить:
// user.id = 2; // Error
// Але у скомпільованому JS:
// user.id = 2; // OK у JavaScript!
TypeScript не додає runtime-захист. Якщо потрібна справжня незмінність у runtime, використовуйте Object.freeze():
let user = Object.freeze({ id: 1, name: "Alice" });
user.id = 2; // Помилка у strict mode JS
Коли ви пишете let items = [], TypeScript не має інформації про тип майбутніх елементів, тому виводить any[]:
let items = []; // тип: any[]
items.push(1); // OK
items.push("text"); // OK — небезпечно!
Це вимикає перевірку типів. Завжди типізуйте порожні масиви явно:
let items: number[] = []; // тип: number[]
items.push(1); // OK
items.push("text"); // Error: тип string не підходить
Примітивні типи даних: string, number, boolean
Глибокий розбір примітивних типів string, number, boolean з поясненням їхньої внутрішньої будови, особливостей JavaScript та типізації у TypeScript
Union типи, Intersection типи та літеральні типи
Глибокий розбір union типів, intersection типів та літеральних типів у TypeScript: комбінування типів, звуження та практичне застосування