Avatar ·

Как динамически добавлять свойства объекту в TypeScript без ошибок типов?

Всем привет! Я новичок в TypeScript и столкнулся с проблемой при работе с объектами. У меня есть пустой объект, который я хочу наполнять свойствами динамически, в зависимости от условий. Например, я получаю данные из API, и в зависимости от ответа мне нужно добавить те или иные поля в объект. Вот упрощённый пример того, что я пытаюсь сделать: ```typescript const user = {}; if (someCondition) { user.name = 'Алексей'; // Ошибка: Property 'name' does not exist on type '{}' } if (anotherCondition) { user.age = 30; // Ошибка: Property 'age' does not exist on type '{}' } ``` TypeScript ругается, что у пустого объекта `{}` нет таких свойств. Я понимаю, что это из-за строгой типизации, но как правильно объявить объект, чтобы можно было добавлять свойства на лету? Я пробовал использовать `any`, но это убивает всю суть TypeScript. Есть ли какой-то более правильный способ? Буду благодарен за примеры кода с пояснениями. Заранее спасибо! <<>>

Решение с помощью Record и Index Signatures

Проблема, с которой вы столкнулись, типична для новичков в TypeScript. Дело в том, что пустой объект {} имеет тип {} — это объект без свойств. TypeScript строго проверяет, что вы не обращаетесь к несуществующим полям. Чтобы решить эту задачу, нужно явно указать, что объект может содержать динамические свойства. Есть несколько корректных подходов.

1. Использование Record (рекомендуемый способ)

Record<K, V> — это встроенный тип, который создаёт объект с ключами типа K и значениями типа V.

// Объявляем объект, где ключи — строки, значения — любые (string | number)
const user: Record<string, string | number> = {};

if (someCondition) {
  user.name = 'Алексей'; // ✅ Работает
}

if (anotherCondition) {
  user.age = 30; // ✅ Работает
}

console.log(user.name); // 'Алексей'
console.log(user.age);  // 30

Плюсы: Простота, читаемость, типизация значений. Минус: Все ключи должны быть одного типа (в примере — string | number).

2. Использование Index Signature (сигнатура индекса)

Можно указать, что объект может иметь любое количество свойств с определённым типом значения.

interface User {
  [key: string]: string | number | undefined;
}

const user: User = {};

if (someCondition) {
  user.name = 'Алексей'; // ✅
}

if (anotherCondition) {
  user.age = 30; // ✅
}

[key: string]: ... означает: «объект может иметь свойства с любыми строковыми ключами». undefined добавлен, чтобы можно было проверять существование свойства: if (user.name !== undefined).

3. Частичное использование (Partial) с заранее известными полями

Если вы знаете, какие свойства могут появиться, но не все сразу, используйте Partial<T>.

interface UserProfile {
  name: string;
  age: number;
  email: string;
}

const user: Partial<UserProfile> = {};

if (someCondition) {
  user.name = 'Алексей'; // ✅
}

if (anotherCondition) {
  user.age = 30; // ✅
}

// user.email остаётся undefined, но это нормально

4. Тип any (не рекомендуется, но для справки)

const user: any = {};
user.name = 'Алексей'; // ✅, но теряется проверка типов

Что выбрать?

  • Если ключи и типы значений заранее неизвестны — используйте Record<string, any> или {[key: string]: any}. Но лучше избегать any.
  • Если типы значений разные (строка, число, boolean) — Record<string, string | number | boolean>.
  • Если вы знаете структуру, но не уверены, какие поля будут — Partial<YourInterface>.
  • Если вам нужна максимальная гибкость — index signature с unknown и проверками: {[key: string]: unknown}.

Бонус: безопасное чтение динамических свойств

Чтобы избежать ошибок при чтении, используйте проверку:

if ('name' in user) {
  console.log(user.name); // TypeScript знает, что свойство существует
}

// Или через Optional Chaining
console.log(user?.name); // undefined, если нет

Теперь ваш код будет работать без ошибок типов, а TypeScript продолжит защищать вас от опечаток и неверных типов значений.

<<>>

Альтернативный подход: использование Map

Если ваша задача — динамически добавлять свойства, где ключи — строки, а значения — любого типа, возможно, вам подойдёт встроенная структура Map. Это не объект, но часто решает ту же задачу с лучшей типобезопасностью.

// Создаём Map, где ключи — строки, значения — string | number
const user = new Map<string, string | number>();

if (someCondition) {
  user.set('name', 'Алексей'); // ✅
}

if (anotherCondition) {
  user.set('age', 30); // ✅
}

// Чтение
console.log(user.get('name')); // 'Алексей' | undefined
console.log(user.get('age'));  // 30 | undefined

Преимущества Map перед объектом:

  • Типизация и ключей, и значений.
  • Методы .set(), .get(), .has(), .delete() — удобнее для динамической работы.
  • Нет проблем с прототипом (например, toString не будет мешать).
  • Итерация через for...of или .forEach().

Недостаток: Это не объект, поэтому если вам нужен именно объект (например, для JSON.stringify или передачи в библиотеку, ожидающую объект), придётся конвертировать:

const obj = Object.fromEntries(user);
console.log(obj); // { name: 'Алексей', age: 30 }

Если ваша задача — просто хранить динамические данные с типизацией, Map — отличный выбор.