Массивы объектов в TypeScript: пошаговый туториал для тех, кто устал от ошибок
Вы когда-нибудь писали на TypeScript и вдруг получали красное подчеркивание от IDE, когда пытались добавить объект в массив? Знакомо, правда? Вопрос "How can I define an array of objects?" — один из самых популярных на Stack Overflow. И не зря: без правильной типизации массивов объектов ваш код превращается в источник багов и головной боли.
В этом туториале мы разберем все способы определения массива объектов в TypeScript — от простых до продвинутых. Вы узнаете, как избежать распространенных ошибок, как использовать интерфейсы и как сделать ваш код не только рабочим, но и красивым. Поехали!
Почему просто [] не работает?
Допустим, вы пишете:
let users = [];
TypeScript видит это как never[] — массив, который никогда не должен содержать элементов. Попробуйте добавить туда объект:
users.push({ name: 'Анна', age: 25 }); // Ошибка!
IDE закричит: "Argument of type '{ name: string; age: number; }' is not assignable to parameter of type 'never'." TypeScript не знает, что вы собираетесь хранить в массиве, поэтому блокирует любые добавления.
Решение — явно указать тип массива. Давайте разберем все варианты.
Способ 1: Базовая аннотация типа — квадратные скобки
Самый простой и интуитивный способ — использовать синтаксис Тип[]:
let users: { name: string; age: number }[] = [];
users.push({ name: 'Анна', age: 25 }); // ✅ Работает
users.push({ name: 'Иван', age: 30 }); // ✅ Работает
Здесь мы говорим TypeScript: "Эй, это массив объектов, у каждого из которых есть свойства name (строка) и age (число)". IDE теперь довольна, автодополнение работает, ошибки типов отлавливаются на этапе компиляции.
Минус: если структура объекта сложная, описание прямо в аннотации становится громоздким. Для таких случаев придумали интерфейсы.
Способ 2: Интерфейсы — ваш лучший друг
Интерфейсы позволяют вынести описание объекта в отдельную сущность и переиспользовать её. Это делает код чище и понятнее:
interface User {
id: number;
name: string;
age: number;
email?: string; // необязательное свойство
}
let users: User[] = [];
users.push({ id: 1, name: 'Анна', age: 25 });
users.push({ id: 2, name: 'Иван', age: 30, email: 'ivan@example.com' }); // ✅
Теперь если вы попытаетесь добавить объект без поля id или с неправильным типом, TypeScript выдаст ошибку. Интерфейсы особенно полезны, когда один и тот же тип объекта используется в нескольких местах.
А что насчет type?
Вместо interface можно использовать type:
type User = {
id: number;
name: string;
age: number;
};
let users: User[] = [];
Разница между interface и type — тема для отдельной статьи. Для массивов объектов можно использовать оба варианта, но interface чаще применяется для описания форм объектов.
Способ 3: Generic-синтаксис Array<Type>
Альтернативный способ — использовать дженерики:
let users: Array<{ name: string; age: number }> = [];
// Или с интерфейсом:
let users: Array<User> = [];
Оба синтаксиса (Type[] и Array<Type>) работают одинаково. Выбор — дело вкуса и контекста. Лично я предпочитаю Type[] для простых типов и Array<Type> для сложных дженерик-конструкций. Но главное — быть последовательным в проекте.
Способ 4: Создание массива с начальными объектами
Часто массив нужно не просто объявить, но и сразу наполнить данными. TypeScript сам выведет тип из литерала:
let users = [
{ id: 1, name: 'Анна', age: 25 },
{ id: 2, name: 'Иван', age: 30 }
]; // Тип выведен: { id: number; name: string; age: number }[]
Но будьте осторожны: если все объекты имеют одинаковую структуру, выведение типа работает идеально. Если структуры различаются, TypeScript может вывести объединение типов (union type), что не всегда желательно.
let mixed = [
{ id: 1, name: 'Анна' },
{ id: 2, name: 'Иван', age: 30 }
]; // Тип: { id: number; name: string } | { id: number; name: string; age: number }[]
Лучше явно указать тип массива, если структура объектов неоднородна.
Способ 5: Вложенные массивы и сложные объекты
Объекты могут содержать другие объекты или массивы. Интерфейсы справляются и с этим:
interface Post {
id: number;
title: string;
author: User; // другой интерфейс
tags: string[]; // массив строк
comments: Comment[]; // массив объектов Comment
}
interface Comment {
id: number;
text: string;
userId: number;
}
let posts: Post[] = [
{
id: 1,
title: 'TypeScript для начинающих',
author: { id: 1, name: 'Анна', age: 25 },
tags: ['typescript', 'tutorial'],
comments: [
{ id: 1, text: 'Отличная статья!', userId: 2 }
]
}
];
TypeScript проверит каждый уровень вложенности. Если в комментарии не будет поля text или оно будет числом — компилятор выдаст ошибку. Это экономит часы отладки.
Типизация методов массива
Когда массив правильно типизирован, все методы массива (map, filter, reduce, find) работают с автодополнением:
let users: User[] = [
{ id: 1, name: 'Анна', age: 25 },
{ id: 2, name: 'Иван', age: 30 },
{ id: 3, name: 'Мария', age: 28 }
];
// filter — возвращает отфильтрованный массив того же типа
const adults = users.filter(user => user.age >= 18); // User[]
// map — можно изменить тип
const names = users.map(user => user.name); // string[]
// find — возвращает User | undefined
const user = users.find(u => u.id === 2); // User | undefined
// reduce — тип аккумулятора задается явно
const totalAge = users.reduce((sum, user) => sum + user.age, 0);