Динамические свойства объектов в TypeScript: разбор для новичков

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Как динамически присваивать свойства объекту в TypeScript? Понятный разбор для начинающих

Привет, друг! Если ты когда-нибудь писал на JavaScript, а потом перешел на TypeScript, то наверняка сталкивался с ситуацией, когда нужно добавить новое свойство объекту на лету. В JS это делается в два счета: obj.newProp = 'value'. Но стоит тебе попробовать то же самое в TypeScript — и компилятор начинает ругаться: "Property 'newProp' does not exist on type ...". Знакомо?

Не переживай, это нормально. TypeScript защищает нас от случайных ошибок, но иногда его строгость кажется излишней. В этой статье мы разберем, как правильно и безопасно динамически присваивать свойства объектам. Ты узнаешь несколько проверенных способов — от простых до более изящных. Поехали!

Почему TypeScript ругается?

Прежде чем решать проблему, давай поймем её корень. TypeScript — это надстройка над JavaScript, которая добавляет статическую типизацию. Когда ты объявляешь объект:

const user = { name: 'Анна', age: 25 };

TypeScript автоматически выводит его тип: { name: string; age: number }. Это значит, что объект user может содержать только эти два свойства. Попытка добавить email вызовет ошибку компиляции:

user.email = 'anna@example.com'; // ❌ Ошибка!

TypeScript не знает, что такое свойство может появиться, и блокирует код. Но в реальной жизни часто нужно добавлять свойства динамически — например, при работе с данными из API или формами. Давай посмотрим, как это сделать правильно.

Способ 1: Самый простой — as any

Быстрый, но грязный способ — использовать as any. Он буквально говорит TypeScript: «Отключи проверку типов для этого объекта».

const user: any = { name: 'Анна', age: 25 };
user.email = 'anna@example.com'; // ✅ Работает
user.isAdmin = true;             // ✅ Работает

Почему это плохо? Потому что ты теряешь все преимущества TypeScript: автодополнение, проверку типов, защиту от опечаток. Если случайно написать user.emial, ошибки не будет — ты узнаешь о ней только в рантайме. Используй этот способ только для быстрого прототипирования или когда точно знаешь, что делаешь.

Способ 2: Индексная сигнатура (Index Signature)

Более правильный подход — заранее объявить, что объект может содержать динамические свойства. Для этого используется индексная сигнатура.

interface User {
  name: string;
  age: number;
  [key: string]: any; // 👈 Разрешаем любые дополнительные свойства
}

const user: User = { name: 'Анна', age: 25 };
user.email = 'anna@example.com'; // ✅ Работает
user.phone = '+7-999-123-45-67'; // ✅ Работает

Что здесь происходит? Запись [key: string]: any означает: «у объекта могут быть любые строковые ключи, и их значения могут быть любого типа». Теперь ты можешь добавлять свойства динамически, и TypeScript не будет ругаться.

Важный нюанс: если ты хочешь ограничить типы значений, замени any на конкретный тип:

interface User {
  name: string;
  age: number;
  [key: string]: string | number; // Только строки и числа
}

Индексная сигнатура — отличный выбор, когда ты заранее знаешь, что объект будет расширяться. Но будь осторожен: если в интерфейсе есть другие свойства с другими типами, они тоже должны соответствовать сигнатуре.

Способ 3: Утилитарный тип Record

Если тебе нужно создать объект, где все ключи и значения одного типа, используй встроенный тип Record<Keys, Type>. Он идеально подходит для словарей или маппингов.

type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;

const roles: UserRoles = {
  anna: 'admin',
  ivan: 'user',
};

roles.petr = 'guest';  // ✅ Добавляем динамически
roles.olga = 'admin';  // ✅ Добавляем динамически
// roles.olga = 'superadmin'; // ❌ Ошибка! Только 'admin' | 'user' | 'guest'

Record создает тип объекта, где все ключи — строки (или другой тип, который ты укажешь), а все значения — заданного типа. Это удобно, когда структура однородна.

Пример из жизни: представь, что ты получаешь список пользователей и хочешь сгруппировать их по ролям:

interface User {
  id: number;
  name: string;
  role: 'admin' | 'user';
}

const usersByRole: Record<string, User[]> = {};

usersByRole['admin'] = []; // ✅ Создаем новую группу
usersByRole['user'] = [];  // ✅ Создаем новую группу

Способ 4: Partial и работа с неполными объектами

Иногда ты не знаешь заранее, какие свойства будут у объекта. Например, ты получаешь данные из формы, где пользователь заполнил только часть полей. В этом случае поможет утилитарный тип Partial<T>. Он делает все свойства объекта опциональными.

interface UserForm {
  name: string;
  email: string;
  age?: number; // изначально опционально
}

function updateUser(updates: Partial<UserForm>) {
  // updates может содержать любое подмножество свойств
  console.log(updates);
}

updateUser({ name: 'Анна' });           // ✅ Только name
updateUser({ email: 'a@b.com', age: 30 }); // ✅ Любая комбинация
updateUser({});                          // ✅ Пустой объект тоже можно

Partial делает каждое свойство необязательным. Это не совсем «динамическое присваивание», но это способ работать с объектами, структура которых меняется.

Если хочешь наоборот — сделать все свойства обязательными, используй Required<T>. А если хочешь выбрать только некоторые — Pick<T, K>.

Способ 5: Оператор as для конкретного случая

Бывает, что ты уверен в структуре объекта, но TypeScript не может её вывести. Например, ты парсишь JSON:

const jsonData = '{ "name": "Анна", "age": 25, "email": "anna@example.com" }';
const parsed = JSON.parse(jsonData);

// parsed — any, мы не знаем его структуру
console.log(parsed.email); // ❌ TypeScript не знает про email

Вместо того чтобы использовать as any, лучше создать интерфейс и привести тип:

interface User {
  name: string;
  age: number;
  email?: string;
}

const parsed = JSON.parse(jsonData) as User;
console.log(parsed.email); // ✅ Работает с автодополнением

Блоги

Рекомендаций по книгам