Avatar ·

Клонирование объектов в TypeScript: глубокое и поверхностное копирование

Привет! Я новичок в TypeScript (и JS в целом). У меня есть объект с данными пользователя: ```typescript interface User { name: string; age: number; address: { city: string; street: string; }; hobbies: string[]; } const user: User = { name: 'Анна', age: 28, address: { city: 'Москва', street: 'Тверская' }, hobbies: ['чтение', 'бег'] }; ``` Мне нужно создать копию этого объекта и изменить в ней только `age`, но так, чтобы исходный объект остался без изменений. Я попробовал просто присвоить: ```typescript const userCopy = user; userCopy.age = 30; ``` Но при этом изменился и `user.age`! Потом я использовал спред-оператор: ```typescript const userCopy = { ...user }; userCopy.age = 30; ``` Это сработало для `age`, но когда я попробовал изменить `userCopy.address.city = 'СПб'`, то у `user.address.city` тоже поменялось значение! А ещё я не могу понять, как правильно клонировать вложенные объекты и массивы. Подскажите, как правильно клонировать объекты в TypeScript? Чем поверхностное копирование отличается от глубокого? И какие есть способы сделать полную независимую копию со всеми вложенными структурами? Важно, чтобы типы TypeScript сохранялись. <<>>

Поверхностное vs глубокое копирование

Вы столкнулись с классической проблемой JavaScript/TypeScript. Когда вы пишете const userCopy = user, вы не создаёте копию — вы создаёте ссылку на тот же объект. Любое изменение через userCopy меняет исходный объект.

Спред-оператор { ...user } делает поверхностное копирование (shallow copy): он копирует только первый уровень свойств. Для примитивов (string, number, boolean) это работает — создаются независимые копии. Но для вложенных объектов и массивов копируются ссылки на те же самые внутренние структуры. Поэтому изменение address.city в копии затрагивает оригинал.

Способы глубокого копирования

Вот основные методы, которые работают в TypeScript:

  • JSON.parse/JSON.stringify — самый простой способ, но с ограничениями
  • structuredClone — современный нативный метод (доступен в Node 17+, современных браузерах)
  • Рекурсивная функция — полный контроль над процессом
  • Библиотеки — lodash.cloneDeep для сложных случаев

Пример 1: JSON-метод (простой, но с ограничениями)

function deepCloneJSON<T>(obj: T): T {
  return JSON.parse(JSON.stringify(obj));
}

const userCopy = deepCloneJSON(user);
userCopy.address.city = 'СПб';
console.log(user.address.city); // 'Москва' — оригинал не изменился

Минусы: теряет функции, undefined, Symbol, специальные объекты (Date, Map, Set превращаются в строки), не работает с циклическими ссылками.

Пример 2: structuredClone (рекомендуемый способ)

const userCopy: User = structuredClone(user);
userCopy.address.city = 'СПб';
userCopy.hobbies.push('плавание');
console.log(user.hobbies); // ['чтение', 'бег'] — без изменений

Преимущества: поддерживает Date, Map, Set, ArrayBuffer, циклические ссылки. Работает быстрее JSON-метода. TypeScript корректно выводит тип.

Пример 3: Рекурсивная функция с поддержкой типов

function deepClone<T>(obj: T, seen = new WeakMap()): T {
  if (obj === null || typeof obj !== 'object') return obj;
  if (seen.has(obj)) return seen.get(obj);
  
  if (obj instanceof Date) return new Date(obj.getTime()) as any;
  if (obj instanceof Array) {
    const arrCopy = obj.map(item => deepClone(item, seen)) as any;
    seen.set(obj, arrCopy);
    return arrCopy;
  }
  
  const clone = Object.create(Object.getPrototypeOf(obj));
  seen.set(obj, clone);
  
  for (const key of Object.keys(obj)) {
    clone[key] = deepClone((obj as any)[key], seen);
  }
  return clone;
}

const userCopy: User = deepClone(user);

Что выбрать?

  • Для простых объектов без специальных типов — structuredClone (или JSON, если нужна поддержка старых сред)
  • Для максимальной производительности — structuredClone
  • Для полного контроля и поддержки редких кейсов — рекурсивная функция
  • Для легаси-проектов — _.cloneDeep из lodash

Важно: все эти методы создают независимую копию. Изменения в копии никак не влияют на оригинал, даже на глубоких уровнях вложенности.

<<>>

Альтернативный подход: библиотеки и утилиты

Если вы не хотите писать собственные реализации, используйте проверенные библиотеки. Самый популярный вариант — lodash с методом cloneDeep:

import cloneDeep from 'lodash/cloneDeep';

const userCopy: User = cloneDeep(user);
userCopy.address.city = 'СПб'; // оригинал не меняется

Плюсы: проверен годами, поддерживает все типы (Date, Map, Set, RegExp, Buffer), обрабатывает циклические ссылки, работает в Node.js и браузерах.

Минусы: добавляет зависимость (хотя можно импортировать только один метод, а не всю библиотеку).

Когда использовать поверхностное копирование

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

interface SimpleUser {
  name: string;
  age: number;
}

const simpleUser: SimpleUser = { name: 'Иван', age: 25 };
const copy = { ...simpleUser };
copy.age = 30; // оригинал не меняется

Для массивов объектов с одним уровнем вложенности можно использовать map со спредом:

const users: User[] = [/* ... */];
const usersCopy = users.map(u => ({ ...u })); // поверхностно

Но помните: если внутри объектов есть вложенные структуры, поверхностное копирование не спасёт — используйте глубокое.