Массивы и кортежи (tuples)
Типизация массивов, фиксированные кортежи — как TypeScript помогает работать с коллекциями данных безопасно и удобно
1. Что такое массив — аналогия с полкой
Друзья, давайте начнём с самого простого. Представьте, что у вас есть полка в шкафу. На этой полке вы можете хранить книги. Каждая книга стоит рядом с другой — и все они на одной полке. Массив в программировании — это именно такая «полка для данных».
Массив — это упорядоченная коллекция элементов одного типа. Все элементы стоят «рядком», и к каждому можно обратиться по номеру. Нумерация начинается с нуля — это важно запомнить!
Аналогия: Если полка — это массив, то каждая книга на ней — это элемент массива. Книга под номером 0 — первая, под номером 1 — вторая, и так далее.
В JavaScript массивы создаются через квадратные скобки. Вот так:
// Простой массив с числами
const numbers: number[] = [1, 2, 3, 4, 5];
// Массив со строками
const fruits: string[] = ["яблоко", "банан", "вишня"];
// Пустой массив
const emptyArray: number[] = [];
// Массив с булевыми значениями
const flags: boolean[] = [true, false, true, true];
// Обращение к элементу по индексу
console.log(numbers[0]); // 1 (первый элемент)
console.log(fruits[1]); // "банан" (второй элемент)
// Длина массива
console.log(numbers.length); // 5
Здесь : number[] — это аннотация типа, которая говорит TypeScript: «эта переменная — массив, и в ней могут быть только числа».
Давайте посмотрим на это подробнее. Если вы попытаетесь положить строку в массив чисел, TypeScript сразу скажет, что это ошибка:
// numbers — массив чисел
const numbers: number[] = [1, 2, 3];
// Это будет ошибкой!
numbers.push("привет"); // Ошибка: тип "string" не может быть назначен типу "number"
numbers.push(true); // Ошибка: тип "boolean" не может быть назначен типу "number"
// А это работает нормально
numbers.push(4); // OK
numbers.push(100); // OK
Почему это важно? В обычном JavaScript вы можете случайно добавить в массив что угодно — строку, объект, null, undefined. Это приводит к трудноуловимым багам. TypeScript защищает вас от этого!
Давайте рассмотрим несколько практических примеров, чтобы вы лучше поняли, как работают массивы:
// Список покупок
const shoppingList: string[] = [
"молоко",
"хлеб",
"сыр",
"яйца",
"масло"
];
// Оценки учеников
const grades: number[] = [5, 4, 5, 3, 5, 4, 4, 5];
// Результаты тестов (прошёл/не прошёл)
const testResults: boolean[] = [true, true, false, true, true, false];
// Выведем каждый элемент
for (let i = 0; i < shoppingList.length; i++) {
console.log(`${i + 1}. ${shoppingList[i]}`);
}
// Вывод:
// 1. молоко
// 2. хлеб
// 3. сыр
// 4. яйца
// 5. масло
Как видите, массивы очень удобны для хранения списков. Но в TypeScript главная сила массивов — это их типизация. Вы всегда знаете, какой тип данных лежит в массиве, и компилятор не даст вам случайно нарушить этот контракт.
2. Типизация массивов: Type[] и Array<Type>
В TypeScript есть два способа указать, что переменная — это массив определённого типа. Оба способа абсолютно равнозначны и работают одинаково. Выбирайте тот, который вам больше нравится!
Способ 1: Синтаксис с квадратными скобками (Type[])
Это самый распространённый и краткий способ. Вы пишете тип, а потом квадратные скобки:
// Массив чисел — способ 1
const scores: number[] = [10, 20, 30];
// Массив строк — способ 1
const names: string[] = ["Анна", "Борис", "Виктор"];
// Массив булевых — способ 1
const isActive: boolean[] = [true, false, true];
Способ 2: Обобщённый синтаксис (Array<Type>)
Этот способ использует обобщения (generics) — специальный механизм TypeScript. Здесь тип передаётся в угловых скобках:
// Массив чисел — способ 2
const scores: Array<number> = [10, 20, 30];
// Массив строк — способ 2
const names: Array<string> = ["Анна", "Борис", "Виктор"];
// Массив булевых — способ 2
const isActive: Array<boolean> = [true, false, true];
Как видите, оба способа дают одинаковый результат. Разница только в синтаксисе:
number[] — короткий, современный, чаще используется
Array<number> — более «явный», иногда удобнее для чтения
Рекомендация: большинство разработчиков предпочитают number[],потому что это короче.
Давайте рассмотрим более сложные примеры, гдеэти дваспособвозможнонепроявляет преимущество:
// Массив объектов — когда полезен Array
interface User {
name: string;
age: number;
}
// Оба варианта работают:
const users1: User[] = [
{ name: "Анна", age: 25 },
{ name: "Борис", age: 30 }
];
const users2: Array<User> = [
{ name: "Анна", age: 25 },
{ name: "Борис", age: 30 }
];
// Вложенные массивы (массив массивов)
const matrix1: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
const matrix2: Array<Array<number>> = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// Массив кортежей (пока просто посмотрите, позже разберём!)
const coordinates1: [number, number][] = [
[10, 20],
[30, 40],
[50, 60]
];
const coordinates2: Array<[number, number]> = [
[10, 20],
[30, 40],
[50, 60]
];
Совет: Используйте Type[] по умолчанию. Переключайтесь на Array<Type> только когда это делает код читаемее (например, в сложных вложенных типах).
Теперь давайте посмотрим, как работают методы массивов с типизацией. TypeScript «знает» тип элементов и автоматически определяет тип результата:
// Метод map — возвращает НОВЫЙ массив
const numbers: number[] = [1, 2, 3, 4, 5];
// TypeScript автоматически понимает, что результат — массив строк
const doubled = numbers.map(n => n * 2); // number[]
const asStrings = numbers.map(n => `Число: ${n}`); // string[]
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(asStrings); // ["Число: 1", "Число: 2", ...]
// Метод filter — возвращает новый массив с тем же типом
const evens = numbers.filter(n => n % 2 === 0); // number[]
console.log(evens); // [2, 4]
// Метод find — возвращает один элемент или undefined
const found = numbers.find(n => n > 3); // number | undefined
console.log(found); // 4
// Метод reduce — возвращает результат любого типа
const sum = numbers.reduce((acc, n) => acc + n, 0); // number
console.log(sum); // 15
// Метод some / every — возвращают boolean
const hasEven = numbers.some(n => n % 2 === 0); // boolean
const allPositive = numbers.every(n => n > 0); // boolean
console.log(hasEven); // true
console.log(allPositive); // true
3. Типизация смешанных массивов
Иногда вам нужно хранить в массиве данные разных типов. Например, вы хотите создать массив, в котором лежат и числа, и строки. TypeScript позволяет это сделать с помощью объединённых типов (union types).
Объединение типов (Union Types)
Чтобы указать, что массив может содержать элементы нескольких типов, используют символ вертикальной черты |:
// Массив, который может содержать числа ИЛИ строки
const mixed: (number | string)[] = [1, "два", 3, "четыре"];
// Массив, который может содержать числа, строки ИЛИ булевы
const multiType: (number | string | boolean)[] = [1, "привет", true, 42];
// Массив с null и undefined
const withNull: (string | null)[] = ["привет", null, "мир"];
const withUndefined: (number | undefined)[] = [1, undefined, 3];
// Массив с любым типом (не рекомендуется!)
const anyArray: any[] = [1, "строка", true, { name: "объект" }];
Осторожно! Использование any[] отключает типизацию для этого массива. Это противоречит философии TypeScript. Старайтесь использовать конкретные типы!
Когда вы обращаетесь к элементу смешанного массива, TypeScript «не знает», какой именно тип у этого элемента. Поэтому вам нужно проверять тип перед использованием:
// Смешанный массив
const mixed: (number | string)[] = [1, "два", 3, "четыре"];
// TypeScript НЕ позволяет делать так:
// console.log(mixed[0].toUpperCase()); // Ошибка! number не имеет toUpperCase
// Нужно сначала проверить тип
const firstElement = mixed[0];
if (typeof firstElement === "string") {
console.log(firstElement.toUpperCase()); // OK! TypeScript "знает", что это строка
} else {
console.log(firstElement.toFixed(2)); // OK! TypeScript "знает", что это число
}
// Или использовать приведение типов
const secondElement = mixed[1] as string;
console.log(secondElement.toUpperCase()); // "ДВА"
// Можно перебрать массив и обработать каждый тип
mixed.forEach(item => {
if (typeof item === "number") {
console.log(`Число: ${item * 2}`);
} else {
console.log(`Строка: ${item.toUpperCase()}`);
}
});
Практический пример: массив данных формы
Давайте создадим более реалистичный пример — массив полей формы, где каждое поле может быть строкой, числом или булевым значением:
// Тип для значения поля формы
type FormValue = string | number | boolean;
// Массив полей формы
const formData: FormValue[] = [
"Иван", // имя
25, // возраст
true, // согласие на обработку данных
"ivan@mail.ru", // email
180 // рост в см
];
// Функция для вывода данных формы
function printFormData(data: FormValue[]): void {
data.forEach((value, index) => {
const type = typeof value;
console.log(`Поле ${index + 1} (${type}): ${value}`);
});
}
printFormData(formData);
// Вывод:
// Поле 1 (string): Иван
// Поле 2 (number): 25
// Поле 3 (boolean): true
// Поле 4 (string): ivan@mail.ru
// Поле 5 (number): 180
Массив объектов разных типов
Часто вам нужно хранить в массиве объекты разных структур. Это можно сделать с помощью объединения интерфейсов:
// Разные типы фигур
interface Circle {
kind: "circle";
radius: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Triangle {
kind: "triangle";
base: number;
height: number;
}
// Массив разных фигур
const shapes: (Circle | Rectangle | Triangle)[] = [
{ kind: "circle", radius: 5 },
{ kind: "rectangle", width: 10, height: 20 },
{ kind: "triangle", base: 8, height: 12 },
{ kind: "circle", radius: 3 },
{ kind: "rectangle", width: 5, height: 8 }
];
// Функция для вычисления площади
function calculateArea(shape: Circle | Rectangle | Triangle): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
// Выведем площади всех фигур
shapes.forEach((shape, index) => {
const area = calculateArea(shape);
console.log(`Фигура ${index + 1} (${shape.kind}): площадь = ${area.toFixed(2)}`);
});
4. Readonly массивы
Иногда вы хотите создать массив, который нельзя изменить после создания. Это как «заклеить» полку клейкой лентой — ничего нельзя добавить, удалить или переместить. В TypeScript для этого есть модификатор readonly.
// Обычный массив — можно изменять
const mutableArray: number[] = [1, 2, 3];
mutableArray.push(4); // OK
mutableArray[0] = 100; // OK
mutableArray.pop(); // OK
// Readonly массив — нельзя изменять!
const readonlyArray: readonly number[] = [1, 2, 3];
// Все эти операции вызовут ошибку:
// readonlyArray.push(4); // Ошибка! Метод "push" не существует в типе "readonly number[]"
// readonlyArray[0] = 100; // Ошибка! Индексная присваиваемость запрещена
// readonlyArray.pop(); // Ошибка! Метод "pop" не существует
// readonlyArray.splice(0, 1); // Ошибка! Метод "splice" не существует
// Но ЧИТАТЬ можно:
console.log(readonlyArray[0]); // 1
console.log(readonlyArray.length); // 3
// Можно перебирать
readonlyArray.forEach(item => console.log(item)); // OK
// Можно использовать методы, которые не изменяют массив:
const doubled = readonlyArray.map(n => n * 2); // OK, возвращает новый массив
const sum = readonlyArray.reduce((acc, n) => acc + n, 0); // OK
Альтернативный синтаксис: ReadonlyArray<Type>
Есть ещё один способ создать readonly массив — с помощью обобщённого типа:
// Тот же самый readonly массив, другой синтаксис
const readonlyArray2: ReadonlyArray<number> = [1, 2, 3];
// Аналогичные ошибки:
// readonlyArray2.push(4); // Ошибка!
// readonlyArray2[0] = 10; // Ошибка!
// Константные массивы автоматически readonly
const constArray = [1, 2, 3] as const;
// TypeScript автоматически делает его readonly number[]
// constArray.push(4); // Ошибка!
Когда использовать readonly массивы?
// 1. Константы — данные, которые не должны меняться
const DAYS_OF_WEEK: readonly string[] = [
"Понедельник", "Вторник", "Среда",
"Четверг", "Пятница", "Суббота", "Воскресенье"
];
// 2. Конфигурации — настройки, которые не меняются
const ALLOWED_ORIGINS: readonly string[] = [
"https://example.com",
"https://app.example.com"
];
// 3. Значения перечислений (enum-подобные константы)
const HTTP_STATUS: readonly number[] = [200, 201, 204, 301, 302, 400, 401, 403, 404, 500];
// 4. Когда передаёте массив в функцию и хотите гарантировать,
// что функция его не изменит
function sumArray(numbers: readonly number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
// Функция не может изменить numbers — это безопасно!
}
const scores = [10, 20, 30];
console.log(sumArray(scores)); // 60
Совет: Используйте readonly для данных, которые не должны изменяться. Это помогает избежать случайных ошибок и делает код более предсказуемым.
5. Типизация пустых массивов
Пустой массив — это частый случай. Вы создаёте пустой массив, а потом постепенно наполняете его данными. Но какой тип у пустого массива? TypeScript может вывести это автоматически, но иногда ему нужна ваша помощь.
// TypeScript автоматически понимает тип
const numbers: number[] = []; // пустой массив чисел
numbers.push(1); // OK
numbers.push(2); // OK
// numbers.push("три"); // Ошибка! Ожидается number
const strings: string[] = []; // пустой массив строк
strings.push("привет"); // OK
// strings.push(42); // Ошибка! Ожидается string
// Если не указать тип, TypeScript выведет any[]
const ambiguous = []; // any[]
ambiguous.push(1); // OK
ambiguous.push("два"); // OK — потому что any[]
ambiguous.push(true); // OK — потому что any[]
// Это опасно! Вы потеряли типизацию.
Важно! Всегда указывайте тип для пустых массивов, если TypeScript не может вывести его автоматически. Это предотвратит случайное добавление неправильных данных.
Практические примеры пустых массивов
// Пример 1: Список задач
interface Task {
id: number;
title: string;
completed: boolean;
}
const tasks: Task[] = [];
tasks.push({ id: 1, title: "Купить продукты", completed: false });
tasks.push({ id: 2, title: "Позвонить маме", completed: true });
// Пример 2: Лог ошибок
interface ErrorLog {
timestamp: Date;
message: string;
level: "info" | "warning" | "error";
}
const errorLogs: ErrorLog[] = [];
errorLogs.push({
timestamp: new Date(),
message: "Сервер не отвечает",
level: "error"
});
// Пример 3: Результаты вычислений
const results: number[] = [];
for (let i = 1; i <= 10; i++) {
results.push(i ** 2);
}
console.log(results); // [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
// Пример 4: Коллекция пользователей
interface User {
name: string;
email: string;
}
const users: User[] = [];
// Наполняем данные из какого-то источника
const rawData = [
{ name: "Анна", email: "anna@mail.com" },
{ name: "Борис", email: "boris@mail.com" }
];
rawData.forEach(item => {
users.push({ name: item.name, email: item.email });
});
console.log(users); // [{ name: "Анна", email: "anna@mail.com" }, ...]
6. Что такое кортеж (tuple)
Теперь давайте перейдём к кортежам — это особый вид массивов, который имеет фиксированную длину и фиксированные типы для каждого элемента.
Представьте обычный массив как полку, на которой стоят книги. А кортеж — это как шахматная доска: ровно 8×8 клеток, и в каждой клетке может быть только определённая фигура.
Аналогия: Обычный массив — это список покупок (может содержать что угодно, длина может меняться). Кортеж — это рецепт (точный набор ингредиентов в определённом порядке).
Массив vs Кортеж: ключевые отличия
Обычный массив (number[]):
- Длина может быть любой
- Все элементы одного типа
- Можно добавлять и удалять элементы
- Нельзя знать точный тип каждого элемента по позиции
Кортеж ([number, string]):
- Длина фиксирована
- Каждый элемент имеет свой тип
- Нельзя добавлять или удалять элементы
- Тип каждого элемента известен по позиции
7. Синтаксис кортежей
Синтаксис кортежей похож на обычный массив, но типы перечисляются для каждого элемента отдельно:
// Кортеж из двух элементов: строка и число
const person: [string, number] = ["Анна", 25];
// Кортеж из трёх элементов
const record: [string, number, boolean] = ["Запись", 42, true];
// Кортеж с более сложными типами
const point: [number, number, number] = [10, 20, 30];
// Кортеж с необязательными элементами
const optionalTuple: [string, number?] = ["Анна"]; // OK
const optionalTuple2: [string, number?] = ["Анна", 25]; // OK
// Кортеж с rest-элементом (rest tuple)
const restTuple: [string, ...number[]] = ["Анна", 1, 2, 3, 4];
Обращение к элементам кортежа
// Кортеж: [имя, возраст, email]
const user: [string, number, string] = ["Анна", 25, "anna@mail.com"];
// TypeScript "знает" тип каждого элемента:
const name: string = user[0]; // OK! Это строка
const age: number = user[1]; // OK! Это число
const email: string = user[2]; // OK! Это строка
// Можно обращаться по индексу:
console.log(user[0]); // "Анна"
console.log(user[1]); // 25
console.log(user[2]); // "anna@mail.com"
// НО! Нельзя обращаться по несуществующему индексу:
// console.log(user[3]); // Ошибка! Индекс 3 не существует в типе "[string, number, string]"
Нельзя добавлять или удалять элементы
const point: [number, number] = [10, 20];
// Все эти операции вызовут ошибку:
// point.push(30); // Ошибка! Метод "push" не существует в типе "[number, number]"
// point.pop(); // Ошибка! Метод "pop" не существует
// point.splice(0, 1); // Ошибка! Метод "splice" не существует
// point[2] = 30; // Ошибка! Индекс 2 не существует
// Но МОЖНО изменять существующие элементы:
point[0] = 15; // OK! Заменяем первый элемент
point[1] = 25; // OK! Заменяем второй элемент
console.log(point); // [15, 25]
Важно! Кортежи — это фиксированные структуры. Если вам нужно добавлять элементы, используйте обычный массив. Если вам нужна строгая структура — кортеж.
Практические примеры кортежей
// Пример 1: Координаты точки на карте
const coordinates: [number, number] = [55.7558, 37.6173]; // Москва
// Пример 2: Результат функции (успех, данные)
function divide(a: number, b: number): [boolean, number] {
if (b === 0) {
return [false, 0];
}
return [true, a / b];
}
const [success, result] = divide(10, 2);
console.log(`Успех: ${success}, Результат: ${result}`); // Успех: true, Результат: 5
// Пример 3: Запись в логе
type LogEntry = [Date, string, "info" | "warning" | "error"];
const logs: LogEntry[] = [
[new Date(), "Сервер запущен", "info"],
[new Date(), "Мало памяти", "warning"],
[new Date(), "Ошибка подключения", "error"]
];
// Пример 4: Строка-значение (key-value)
type KeyValue = [string, any];
const settings: KeyValue[] = [
["theme", "dark"],
["language", "ru"],
["fontSize", 14],
["notifications", true]
];
// Пример 5: Диапазон значений
type Range = [number, number];
const ageRange: Range = [0, 120];
const temperatureRange: Range = [-50, 50];
const percentageRange: Range = [0, 100];
Кортеж vs Интерфейс: что выбрать?
// Вариант 1: Кортеж
type UserTuple = [string, number, string];
const user1: UserTuple = ["Анна", 25, "anna@mail.com"];
// Вариант 2: Интерфейс
interface UserInterface {
name: string;
age: number;
email: string;
}
const user2: UserInterface = {
name: "Борис",
age: 30,
email: "boris@mail.com"
};
// Кортеж удобнее, когда:
// - Порядок элементов важен и фиксирован
// - Элементы разного типа
// - Нужна компактная запись
// Интерфейс удобнее, когда:
// - Имена элементов важнее их порядка
// - Элементов много
// - Нужно добавлять/удалять свойства
8. Readonly кортежи
Readonly кортежи — это кортежи, которые нельзя изменить после создания. Это как «заклеить» рецепт клейкой лентой — вы можете его читать, но не можете менять.
// Обычный кортеж — можно изменять элементы
const mutableTuple: [string, number] = ["Анна", 25];
mutableTuple[0] = "Борис"; // OK!
mutableTuple[1] = 30; // OK!
// Readonly кортеж — нельзя изменять!
const readonlyTuple: readonly [string, number] = ["Анна", 25];
// Эти операции вызовут ошибку:
// readonlyTuple[0] = "Борис"; // Ошибка! Индексная присваиваемость запрещена
// readonlyTuple[1] = 30; // Ошибка! Индексная присваиваемость запрещена
// readonlyTuple.push("ещё"); // Ошибка! Метод "push" не существует
// Но ЧИТАТЬ можно:
console.log(readonlyTuple[0]); // "Анна"
console.log(readonlyTuple[1]); // 25
// Можно деструктуризировать:
const [name, age] = readonlyTuple;
console.log(name, age); // "Анна" 25
Readonly кортежи с const
// Использование "as const" создаёт readonly кортеж
const coordinates = [55.7558, 37.6173] as const;
// TypeScript понимает это как readonly [number, number]
// Нельзя изменить:
// coordinates[0] = 0; // Ошибка!
// Но можно использовать в функциях:
function getDistance(
point1: readonly [number, number],
point2: readonly [number, number]
): number {
const dx = point2[0] - point1[0];
const dy = point2[1] - point1[1];
return Math.sqrt(dx ** 2 + dy ** 2);
}
const moscow: readonly [number, number] = [55.7558, 37.6173];
const spb: readonly [number, number] = [59.9343, 30.3351];
console.log(getDistance(moscow, spb)); // ~634 км
Практический пример: неизменяемые данные
// Конфигурация приложения — не должна меняться
const AppConfig: readonly [string, number, boolean] = [
"MyApp", // название
1, // версия
true // режим отладки
];
// Цвета темы — константы
const ThemeColors: readonly [string, string, string] = [
"#ffffff", // фон
"#000000", // текст
"#3595fd" // акцент
];
// Результаты вычислений, которые не должны изменяться
const Statistics: readonly [number, number, number, number] = [
100, // всего пользователей
85, // активные
10, // неактивные
5 // заблокированные
];
// Функция, которая принимает readonly кортеж
function printUser(
user: readonly [string, number, string]
): void {
console.log(`Имя: ${user[0]}`);
console.log(`Возраст: ${user[1]}`);
console.log(`Email: ${user[2]}`);
}
printUser(["Анна", 25, "anna@mail.com"]);
9. Когда использовать кортежи
Кортежи полезны во многих ситуациях. Давайте рассмотрим основные случаи, когда они gerçekten удобны:
1. Возврат нескольких значений из функции
// Функция возвращает [успех, результат]
function divide(a: number, b: number): [boolean, number] {
if (b === 0) {
return [false, 0];
}
return [true, a / b];
}
const [success, result] = divide(10, 2);
if (success) {
console.log(`Результат: ${result}`); // Результат: 5
} else {
console.log("Деление на ноль!");
}
// Функция возвращает [минимум, максимум]
function getMinMax(numbers: number[]): [number, number] {
const min = Math.min(...numbers);
const max = Math.max(...numbers);
return [min, max];
}
const [min, max] = getMinMax([3, 1, 4, 1, 5, 9, 2, 6]);
console.log(`От ${min} до ${max}`); // От 1 до 9
// Функция возвращает [код ошибки, сообщение]
function processRequest(url: string): [number, string] {
if (url.startsWith("http")) {
return [200, "OK"];
}
return [400, "Неверный URL"];
}
const [code, message] = processRequest("https://example.com");
console.log(`${code}: ${message}`); // 200: OK
2. Пары ключ-значение
// Настройки в формате [ключ, значение]
type Setting = [string, string | number | boolean];
const settings: Setting[] = [
["theme", "dark"],
["language", "ru"],
["fontSize", 14],
["notifications", true],
["autoSave", false]
];
// Функция для поиска настройки
function findSetting(
settings: Setting[],
key: string
): Setting | undefined {
return settings.find(([k]) => k === key);
}
const themeSetting = findSetting(settings, "theme");
if (themeSetting) {
console.log(`Тема: ${themeSetting[1]}`); // Тема: dark
}
// Объект настроек
type Config = [string, any][];
const config: Config = [
["apiUrl", "https://api.example.com"],
["timeout", 5000],
["retries", 3],
["debug", false]
];
// Преобразуем в объект
function configToObject(config: Config): Record<string, any> {
const result: Record<string, any> = {};
config.forEach(([key, value]) => {
result[key] = value;
});
return result;
}
console.log(configToObject(config));
// { apiUrl: "https://api.example.com", timeout: 5000, ... }
3. Координаты и геометрические данные
// Точка в 2D пространстве
type Point2D = [number, number];
// Точка в 3D пространстве
type Point3D = [number, number, number];
// Прямоугольник: [x, y, width, height]
type Rectangle = [number, number, number, number];
// Функция для вычисления расстояния
function distance(p1: Point2D, p2: Point2D): number {
const dx = p2[0] - p1[0];
const dy = p2[1] - p1[1];
return Math.sqrt(dx * dx + dy * dy);
}
const pointA: Point2D = [0, 0];
const pointB: Point2D = [3, 4];
console.log(distance(pointA, pointB)); // 5
// Функция для проверки, находится ли точка в прямоугольнике
function isPointInRect(
point: Point2D,
rect: Rectangle
): boolean {
const [px, py] = point;
const [rx, ry, rw, rh] = rect;
return px >= rx && px <= rx + rw && py >= ry && py <= ry + rh;
}
const myPoint: Point2D = [5, 5];
const myRect: Rectangle = [0, 0, 10, 10];
console.log(isPointInRect(myPoint, myRect)); // true
4. История изменений
// Запись об изменении: [время, старое_значение, новое_значение]
type ChangeRecord = [Date, any, any];
const history: ChangeRecord[] = [
[new Date("2024-01-01"), null, "Анна"],
[new Date("2024-01-05"), "Анна", "Анна Иванова"],
[new Date("2024-01-10"), "Анна Иванова", "Анна Петрова"]
];
// Функция для вывода истории
function printHistory(records: ChangeRecord[]): void {
records.forEach(([date, oldVal, newVal]) => {
console.log(`${date.toISOString().split("T")[0]}: ${oldVal} → ${newVal}`);
});
}
printHistory(history);
// 2024-01-01: null → Анна
// 2024-01-05: Анна → Анна Иванова
// 2024-01-10: Анна Иванова → Анна Петрова
5. Результаты валидации
// Результат валидации: [валидно, сообщение_об_ошибке]
type ValidationResult = [boolean, string];
function validateEmail(email: string): ValidationResult {
if (!email.includes("@")) {
return [false, "Email должен содержать @"];
}
if (!email.includes(".")) {
return [false, "Email должен содержать точку"];
}
return [true, "OK"];
}
function validateAge(age: number): ValidationResult {
if (age < 0) {
return [false, "Возраст не может быть отрицательным"];
}
if (age > 150) {
return [false, "Возраст слишком большой"];
}
return [true, "OK"];
}
const emailResult = validateEmail("user@example.com");
if (!emailResult[0]) {
console.log(`Ошибка: ${emailResult[1]}`);
}
const ageResult = validateAge(25);
if (ageResult[0]) {
console.log("Возраст валиден!");
}
10. Деструктуризация
Деструктуризация — это способ «разобрать» массив на отдельные переменные. Это очень удобно, особенно с кортежами!
Деструктуризация обычных массивов
// Обычный массив
const numbers = [10, 20, 30, 40, 50];
// Деструктуризация — извлекаем первые элементы
const [first, second, third] = numbers;
console.log(first); // 10
console.log(second); // 20
console.log(third); // 30
// Пропускаем элементы через запятую
const [, , thirdOnly] = numbers;
console.log(thirdOnly); // 30
// Используем rest-оператор для сбора остатка
const [head, ...tail] = numbers;
console.log(head); // 10
console.log(tail); // [20, 30, 40, 50]
// Значения по умолчанию
const [a = 0, b = 0, c = 0, d = 0, e = 0, f = 0] = numbers;
console.log(f); // 0 (элемента нет — используется значение по умолчанию)
// Пример с функцией
function getUser(): [string, number, string] {
return ["Анна", 25, "anna@mail.com"];
}
const [userName, userAge, userEmail] = getUser();
console.log(`${userName}, ${userAge} лет, ${userEmail}`);
// Анна, 25 лет, anna@mail.com
Деструктуризация кортежей
// Кортеж с именованными переменными — читаемо и понятно
type Person = [name: string, age: number, email: string];
const person: Person = ["Анна", 25, "anna@mail.com"];
// Деструктуризация с именами (TypeScript 4.0+)
const [personName, personAge, personEmail] = person;
console.log(personName); // "Анна"
console.log(personAge); // 25
console.log(personEmail); // "anna@mail.com"
// Именованные элементы кортежа (TypeScript 4.0+)
type NamedTuple = [name: string, age: number];
const user: NamedTuple = ["Борис", 30];
// При деструктуризации имена не используются напрямую,
// но помогают при чтении типа:
function processUser([name, age]: NamedTuple): void {
console.log(`${name} (${age} лет)`);
}
processUser(user); // "Борис (30 лет)"
// Деструктуризация в цикле
const coordinates: [number, number][] = [
[10, 20],
[30, 40],
[50, 60]
];
coordinates.forEach(([x, y]) => {
console.log(`x: ${x}, y: ${y}`);
});
// x: 10, y: 20
// x: 30, y: 40
// x: 50, y: 60
Деструктуризация с переименованием
// Переименование при деструктуризации
const tuple: [string, number] = ["Анна", 25];
// Без переименования
const [name1, age1] = tuple;
// С переименованием (используем "as")
// Но для массивов переименование работает иначе — через промежуточную переменную
const [tempName, tempAge] = tuple;
const userName2 = tempName;
const userAge2 = tempAge;
// Более чистый способ — деструктуризация в параметрах
function greet([personName, personAge]: [string, number]): void {
console.log(`Привет, ${personName}! Тебе ${personAge} лет.`);
}
greet(["Виктор", 35]); // "Привет, Виктор! Тебе 35 лет."
// Деструктуризация с rest-оператором
const mixed: [string, number, ...boolean[]] = ["Анна", 25, true, false, true];
const [mName, mAge, ...mFlags] = mixed;
console.log(mName); // "Анна"
console.log(mAge); // 25
console.log(mFlags); // [true, false, true]
Практический пример: обработка данных
// Данные студентов: [имя, оценки...]
type Student = [string, ...number[]];
const students: Student[] = [
["Анна", 5, 4, 5, 5, 4],
["Борис", 3, 4, 3, 4, 3],
["Виктор", 5, 5, 5, 5, 5],
["Галина", 4, 4, 4, 4, 5]
];
// Функция для вычисления средней оценки
function getAverage(student: Student): number {
const [name, ...grades] = student;
const sum = grades.reduce((acc, g) => acc + g, 0);
return sum / grades.length;
}
// Выведем средние оценки
students.forEach(student => {
const [name, ...grades] = student;
const average = getAverage(student);
console.log(`${name}: средняя оценка ${average.toFixed(2)}`);
});
// Анна: средняя оценка 4.60
// Борис: средняя оценка 3.40
// Виктор: средняя оценка 5.00
// Галина: средняя оценка 4.20
// Функция для поиска лучшего студента
function findBestStudent(students: Student[]): Student {
return students.reduce((best, current) => {
return getAverage(current) > getAverage(best) ? current : best;
});
}
const best = findBestStudent(students);
console.log(`Лучший студент: ${best[0]}`); // "Лучший студент: Виктор"
11. Массивы в функциях
Массивы и кортежи часто используются как параметры и возвращаемые значения функций. Давайте рассмотрим основные паттерны:
Функции, принимающие массивы
// Функция для суммы чисел в массиве
function sumArray(numbers: number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sumArray([1, 2, 3, 4, 5])); // 15
// Функция для фильтрации уникальных значений
function unique<T>(array: T[]): T[] {
return [...new Set(array)];
}
console.log(unique([1, 2, 2, 3, 3, 3])); // [1, 2, 3]
console.log(unique(["а", "б", "а", "в"])); // ["а", "б", "в"]
// Функция для.chunk (разбиение на группы)
function chunk<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
console.log(chunk([1, 2, 3, 4, 5, 6, 7], 3));
// [[1, 2, 3], [4, 5, 6], [7]]
// Функция для поиска в массиве
function findByProperty<T, K extends keyof T>(
array: T[],
property: K,
value: T[K]
): T | undefined {
return array.find(item => item[property] === value);
}
interface User {
name: string;
age: number;
}
const users: User[] = [
{ name: "Анна", age: 25 },
{ name: "Борис", age: 30 }
];
const found = findByProperty(users, "name", "Борис");
console.log(found); // { name: "Борис", age: 30 }
Функции, возвращающие кортежи
// Функция возвращает [минимум, максимум, среднее]
function getStats(numbers: number[]): [number, number, number] {
const min = Math.min(...numbers);
const max = Math.max(...numbers);
const avg = numbers.reduce((a, b) => a + b, 0) / numbers.length;
return [min, max, avg];
}
const [min, max, avg] = getStats([3, 1, 4, 1, 5, 9, 2, 6]);
console.log(`Мин: ${min}, Макс: ${max}, Среднее: ${avg.toFixed(2)}`);
// Мин: 1, Макс: 9, Среднее: 3.88
// Функция возвращает [успех, данные, сообщение об ошибке]
function fetchData<T>(
url: string
): [boolean, T | null, string] {
// Имитация запроса
if (url === "error") {
return [false, null, "Ошибка сети"];
}
return [true, { data: "данные" } as T, ""];
}
const [success, data, error] = fetchData<{ data: string }>("https://api.example.com");
if (success) {
console.log(data?.data); // "данные"
} else {
console.log(error); // "Ошибка сети"
}
// Функция возвращает кортеж с деструктуризацией в параметрах
function processCoordinates(
[x, y]: [number, number],
[dx, dy]: [number, number]
): [number, number] {
return [x + dx, y + dy];
}
const newPos = processCoordinates([10, 20], [5, -3]);
console.log(newPos); // [15, 17]
Rest-параметры и кортежи
// Rest-параметры позволяют принимать любое количество аргументов
function sum(...numbers: number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum()); // 0
// Функция с mix параметрами
function log(level: string, ...messages: string[]): void {
console.log(`[${level}]`, messages.join(" "));
}
log("INFO", "Сервер", "запущен", "на", "порту", "3000");
// [INFO] Сервер запущен на порту 3000
// Rest-параметры с кортежами
function createCoordinates(
x: number,
y: number,
...rest: number[]
): [number, number, ...number[]] {
return [x, y, ...rest];
}
const point3D = createCoordinates(10, 20, 30);
console.log(point3D); // [10, 20, 30]
const point4D = createCoordinates(10, 20, 30, 40);
console.log(point4D); // [10, 20, 30, 40]
Callback-функции с массивами
// Функция higher-order, принимающая callback
function applyToArray<T, R>(
array: T[],
callback: (item: T, index: number) => R
): R[] {
return array.map(callback);
}
const numbers = [1, 2, 3, 4, 5];
const doubled = applyToArray(numbers, (n, i) => n * 2);
const indexed = applyToArray(numbers, (n, i) => `${i}: ${n}`);
console.log(doubled); // [2, 4, 6, 8, 10]
console.log(indexed); // ["0: 1", "1: 2", "2: 3", "3: 4", "4: 5"]
// Функция для группировки
function groupBy<T, K extends string>(
array: T[],
keyFn: (item: T) => K
): Record<K, T[]> {
const result = {} as Record<K, T[]>;
array.forEach(item => {
const key = keyFn(item);
if (!result[key]) {
result[key] = [];
}
result[key].push(item);
});
return result;
}
interface Student {
name: string;
grade: number;
}
const students: Student[] = [
{ name: "Анна", grade: 5 },
{ name: "Борис", grade: 4 },
{ name: "Виктор", grade: 5 },
{ name: "Галина", grade: 4 },
{ name: "Дмитрий", grade: 3 }
];
const grouped = groupBy(students, s => s.grade.toString());
console.log(grouped);
// { "5": [{ name: "Анна", ... }, { name: "Виктор", ... }], ... }
12. Ошибки новичков
Давайте рассмотримсамые частые ошибки, которые делают начинающие при работе с массивами и кортежами:
Ошибка 1: Смешивание типов без объединения
// ОШИБКА: Пытаемся положить строку в массив чисел
const numbers: number[] = [1, 2, 3];
// numbers.push("четыре"); // Ошибка!
// ПРАВИЛЬНО: Используем объединение типов
const mixed: (number | string)[] = [1, 2, "три"];
// Или создаём отдельный тип
type NumberOrString = number | string;
const mixed2: NumberOrString[] = [1, "два", 3];
Ошибка 2: Путаница между массивом и кортежем
// Это МАССИВ — можно добавлять элементы
const array: number[] = [1, 2, 3];
array.push(4); // OK!
// Это КОРТЕЖ — нельзя добавлять элементы
const tuple: [number, number, number] = [1, 2, 3];
// tuple.push(4); // Ошибка!
// Типичная ошибка: пытаемся добавить в кортеж
type Point = [number, number];
// const point: Point = [10, 20];
// point.push(30); // Ошибка! Метод "push" не существует
// ПРАВИЛЬНО: Если нужна изменяемая длина — используйте массив
type PointArray = [number, number, ...number[]];
const point: PointArray = [10, 20];
// point.push(30); // ОК!
Ошибка 3: Неправильный тип индекса
// ОШИБКА: Пытаемся обратиться по несуществующему индексу
const tuple: [string, number] = ["Анна", 25];
// const third = tuple[2]; // Ошибка! Индекс 2 не существует
// ПРАВИЛЬНО: Используйте только доступные индексы
const name = tuple[0]; // OK
const age = tuple[1]; // OK
// Если нужен доступ к произвольному индексу — используйте массив
const array: (string | number)[] = ["Анна", 25];
const item = array[2]; // OK, но тип будет string | number | undefined
Ошибка 4: Забыть указать тип пустого массива
// ОШИБКА: Пустой массив без типа — TypeScript выведет any[]
const items = []; // any[] — опасно!
items.push(1); // OK
items.push("два"); // OK — потеря типизации!
items.push(true); // OK — потеря типизации!
// ПРАВИЛЬНО: Всегда указывайте тип
const numbers: number[] = []; // number[]
numbers.push(1); // OK
// numbers.push("два"); // Ошибка! Защита типизации работает
// Или используйте инициализацию
const strings = ["привет", "мир"]; // TypeScript выведет string[]
const scores = [10, 20, 30]; // TypeScript выведет number[]
Ошибка 5: Неправильное использование readonly
// ОШИБКА: Пытаемся изменить readonly массив
const readonlyArr: readonly number[] = [1, 2, 3];
// readonlyArr.push(4); // Ошибка!
// readonlyArr[0] = 100; // Ошибка!
// ПРАВИЛЬНО: Копируйте, если нужно изменить
const mutableArr = [...readonlyArr]; // Копия — можно изменять
mutableArr.push(4); // OK!
// Или используйте методы, возвращающие новый массив
const doubled = readonlyArr.map(n => n * 2); // Новый массив
const filtered = readonlyArr.filter(n => n > 1); // Новый массив
// Если функция принимает readonly, можно передать обычный массив
function process(arr: readonly number[]): number {
return arr.reduce((a, b) => a + b, 0);
}
const regularArray: number[] = [1, 2, 3];
console.log(process(regularArray)); // OK! Обычный массив совместим с readonly
Ошибка 6: Путаница с типами при деструктуризации
// ОШИБКА: Пытаемся деструктуризировать с неправильным порядком
const tuple: [string, number] = ["Анна", 25];
// Неправильно: порядок переменных не соответствует кортежу
// const [age, name] = tuple; // age будет строкой, name будет числом!
// Правильно: соблюдайте порядок
const [correctName, correctAge] = tuple;
console.log(correctName); // "Анна" (строка)
console.log(correctAge); // 25 (число)
// Если порядок не важен — лучше использовать интерфейс
interface UserProfile {
name: string;
age: number;
}
const user: UserProfile = { name: "Анна", age: 25 };
const { name, age } = user; // Порядок не важен!
Ошибка 7: Использование any[] вместо конкретных типов
// ОШИБКА: Использование any[]
const items: any[] = [1, "два", true, { name: "объект" }];
// Потеряна вся типизация! Компилятор не поймает ошибки.
// ПРАВИЛЬНО: Используйте конкретные типы
const numbers: number[] = [1, 2, 3];
const strings: string[] = ["а", "б", "в"];
// Или объединение типов
const mixed: (number | string)[] = [1, "два", 3];
// Или интерфейс для сложных объектов
interface Item {
id: number;
name: string;
active: boolean;
}
const items2: Item[] = [
{ id: 1, name: "Товар 1", active: true },
{ id: 2, name: "Товар 2", active: false }
];
Ошибка 8: Неправильное сравнение массивов
// ОШИБКА: Сравнение массивов через ===
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
console.log(arr1 === arr2); // false! Сравниваются ссылки, не содержимое
// ПРАВИЛЬНО: Сравнивайте поэлементно
function arraysEqual<T>(a: T[], b: T[]): boolean {
if (a.length !== b.length) return false;
return a.every((item, index) => item === b[index]);
}
console.log(arraysEqual(arr1, arr2)); // true
// Или используйте JSON.stringify для простых случаев
console.log(JSON.stringify(arr1) === JSON.stringify(arr2)); // true
Итоги урока
Что мы изучили:
- Массивы — упорядоченные коллекции элементов одного типа. Обозначаются
Type[]илиArray<Type> - Смешанные массивы — используют объединение типов:
(string | number)[] - Readonly массивы — нельзя изменить после создания:
readonly Type[] - Кортежи — массивы с фиксированной длиной и типами:
[Type1, Type2, ...] - Readonly кортежи — кортежи, которые нельзя изменить:
readonly [Type1, Type2] - Деструктуризация — извлечение элементов в переменные:
const [a, b] = tuple - Массивы в функциях — параметры, возвращаемые значения, rest-параметры
Совет для практики: Попробуйте создать небольшой проект — например, список задач (to-do list) или систему оценок студентов. Используйте массивы для хранения данных и кортежи для пар «ключ-значение» или результатов функций.
Ключевое правило: Используйте обычные массивы, когда длина и типы элементов могут меняться. Используйте кортежи, когда структура данных фиксирована и вы хотите, чтобы TypeScript знал тип каждого элемента по позиции.
Готовы к следующему уроку?
Следующий урок: Enum →Проверяем понимание
8 вопросов