Как динамически добавлять свойства объекту в TypeScript (без ошибок типов)
Проблема и её корень
Вы столкнулись с классической ситуацией, когда TypeScript защищает вас от случайного присвоения свойств, которые не были объявлены в типе объекта. По умолчанию пустой объект {} имеет тип {} — это "самый пустой" тип, который не содержит никаких свойств. Поэтому любая попытка присвоить ему что-то новое вызывает ошибку компиляции.
Основное решение: индексная сигнатура (Index Signature)
Самый правильный способ динамически добавлять свойства — использовать индексную сигнатуру. Она говорит TypeScript: "Этот объект может содержать любые свойства с ключами определённого типа и значениями определённого типа".
Вот как это выглядит на практике:
// Вариант 1: Тип с индексной сигнатурой
interface DynamicObject {
[key: string]: any; // любые строковые ключи, любые значения
}
const data: DynamicObject = {};
data.name = "John"; // ✅ работает
data.age = 30; // ✅ работает
data.isActive = true; // ✅ работает
// Вариант 2: Использование Record<K, V>
const data2: Record<string, any> = {};
data2.name = "Jane"; // ✅ работает
Более точная типизация: конкретные типы значений
Если вы знаете, какие типы значений могут быть у свойств, укажите их точнее вместо any:
interface UserData {
[key: string]: string | number | boolean;
}
const user: UserData = {};
user.name = "Alice"; // ✅ string
user.age = 25; // ✅ number
user.isAdmin = false; // ✅ boolean
user.metadata = {}; // ❌ Ошибка! Объект не входит в тип
Решение для вашего кейса с функцией processItem
В вашем примере можно использовать индексную сигнатуру, но лучше сделать ещё точнее — создать тип, который описывает возможные результаты:
type ProcessedItem = {
[key: string]: string | number | boolean;
};
function processItem(item: any): ProcessedItem {
const result: ProcessedItem = {};
if (item.type === "user") {
result.name = item.value; // ✅ работает
result.role = "member";
} else if (item.type === "product") {
result.price = item.price; // ✅ работает
result.inStock = true;
}
return result;
}
Альтернатива: тип с необязательными полями
Если вы заранее знаете все возможные свойства, но не знаете, какие из них будут установлены, используйте частичный тип (Partial):
interface UserProfile {
name: string;
age: number;
email: string;
}
function createPartialUser(): Partial<UserProfile> {
const user: Partial<UserProfile> = {};
user.name = "Bob"; // ✅ работает
user.age = 30; // ✅ работает
// email не обязателен
return user;
}
Важные замечания и лучшие практики
- Избегайте
any— это отключает всю типизацию. Используйтеunknownили конкретные типы, если возможно. - Используйте
Record<K, V>как более краткую запись для простых случаев. - Для строгой типизации комбинируйте индексную сигнатуру с известными полями:
interface ExtendedObject {
knownField: string; // известное поле
[key: string]: any; // динамические поля
}
Таким образом, вы сохраняете гибкость динамического добавления свойств, но при этом TypeScript продолжает проверять типы значений и защищает вас от случайных ошибок. Главное — выбрать правильный уровень строгости: от полной свободы (Record<string, any>) до точного описания возможных типов значений.
Использование Type Assertion (утверждение типа) как быстрый хак
Если вам нужно быстро решить проблему без создания интерфейсов, можно использовать утверждение типа (type assertion) через as:
const data = {} as { name: string; age: number };
data.name = "John"; // ✅ работает
data.age = 30; // ✅ работает
data.email = "test"; // ❌ Ошибка! Свойство не объявлено
Этот подход полезен, когда вы точно знаете, какие свойства будут добавлены, и хотите сохранить полную типизацию. Однако он не подходит для полностью неизвестной структуры данных.
Использование Object.assign или spread-оператора
Иногда проще не мутировать объект, а создать новый с нужными свойствами:
interface User {
name: string;
age: number;
}
function createUser(name: string, age: number): User {
// Создаём объект сразу со всеми полями
return { name, age };
}
// Или динамически через spread
const base = { type: "user" };
const full = { ...base, name: "John", age: 30 };
// full автоматически имеет тип { type: string; name: string; age: number }
Когда использовать какой подход
- Индексная сигнатура — когда свойства действительно неизвестны заранее (данные из внешних источников).
- Type Assertion — когда вы знаете точную структуру, но создаёте объект поэтапно.
- Spread/assign — когда можно создать объект сразу целиком.
Выбирайте подход в зависимости от ситуации. Для вашего примера с функцией processItem лучше всего подойдёт индексная сигнатура, так как структура результата действительно динамическая.