← Назад к вопросам

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

2.0 Middle🔥 231 комментариев
#JavaScript Core#TypeScript

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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

Эта проблема возникает потому что JSON может передавать только данные, а не метаинформацию о типах, методах класса и других метаданных. Рассмотрим несколько подходов.

Проблема: потеря метаданных при JSON сериализации

// Исходный класс
class User {
  name: string;
  email: string;
  createdAt: Date;

  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
    this.createdAt = new Date();
  }

  getGreeting() {
    return `Hello, ${this.name}!`;
  }
}

const user = new User('John', 'john@example.com');
const json = JSON.stringify(user);

// json содержит: {"name":"John","email":"john@example.com","createdAt":"2024-01-01T..."}

// При десериализации:
const restored = JSON.parse(json);
console.log(restored.getGreeting()); // Ошибка! getGreeting не существует
console.log(restored.createdAt instanceof Date); // false! Это строка, не Date

Решение 1: Кастомные методы сериализации

Используй toJSON и переконструктор:

class User {
  name: string;
  email: string;
  createdAt: Date;

  constructor(name: string, email: string, createdAt?: Date) {
    this.name = name;
    this.email = email;
    this.createdAt = createdAt || new Date();
  }

  // Сериализация
  toJSON() {
    return {
      __type__: 'User', // Метаданные
      name: this.name,
      email: this.email,
      createdAt: this.createdAt.toISOString()
    };
  }

  // Статический метод для десериализации
  static fromJSON(json: any): User {
    if (json.__type__ !== 'User') {
      throw new Error('Invalid type');
    }
    return new User(
      json.name,
      json.email,
      new Date(json.createdAt)
    );
  }

  getGreeting() {
    return `Hello, ${this.name}!`;
  }
}

// Использование
const user = new User('John', 'john@example.com');
const json = JSON.stringify(user);
const restored = User.fromJSON(JSON.parse(json));

console.log(restored.getGreeting()); // "Hello, John!"
console.log(restored.createdAt instanceof Date); // true

Решение 2: Сериализатор/Десериализатор с типами

Создай универсальный сериализатор:

type Constructor<T> = new (...args: any[]) => T;

class Serializer {
  private static typeMap = new Map<string, Constructor<any>>();

  // Регистрация типов
  static register(name: string, type: Constructor<any>) {
    this.typeMap.set(name, type);
  }

  // Сериализация
  static serialize(obj: any): string {
    const serialize = (val: any): any => {
      if (val === null || val === undefined) {
        return val;
      }

      // Date -> ISO string
      if (val instanceof Date) {
        return {
          __type__: 'Date',
          value: val.toISOString()
        };
      }

      // Массивы
      if (Array.isArray(val)) {
        return val.map(serialize);
      }

      // Объекты
      if (typeof val === 'object') {
        const result: any = {
          __type__: val.constructor.name
        };
        for (const [key, value] of Object.entries(val)) {
          result[key] = serialize(value);
        }
        return result;
      }

      return val;
    };

    return JSON.stringify(serialize(obj));
  }

  // Десериализация
  static deserialize<T>(json: string, type: Constructor<T>): T {
    const data = JSON.parse(json);

    const deserialize = (val: any, expectedType?: Constructor<any>): any => {
      if (val === null || val === undefined) {
        return val;
      }

      // Специальный тип Date
      if (val.__type__ === 'Date') {
        return new Date(val.value);
      }

      // Объект с типом
      if (val.__type__ && val.__type__ !== 'Object') {
        const ctor = this.typeMap.get(val.__type__);
        if (!ctor) {
          throw new Error(`Unknown type: ${val.__type__}`);
        }

        const instance = Object.create(ctor.prototype);
        const { __type__, ...fields } = val;

        for (const [key, value] of Object.entries(fields)) {
          instance[key] = deserialize(value);
        }

        return instance;
      }

      // Массивы
      if (Array.isArray(val)) {
        return val.map(deserialize);
      }

      // Простые объекты
      if (typeof val === 'object') {
        const result: any = {};
        for (const [key, value] of Object.entries(val)) {
          result[key] = deserialize(value);
        }
        return result;
      }

      return val;
    };

    return deserialize(data, type);
  }
}

// Регистрация типов
class User {
  name: string;
  email: string;
  createdAt: Date;

  constructor(name: string, email: string, createdAt = new Date()) {
    this.name = name;
    this.email = email;
    this.createdAt = createdAt;
  }

  getGreeting() {
    return `Hello, ${this.name}!`;
  }
}

Serializer.register('User', User);

// Использование
const user = new User('John', 'john@example.com', new Date('2024-01-01'));
const json = Serializer.serialize(user);
const restored = Serializer.deserialize<User>(json, User);

console.log(restored.getGreeting()); // "Hello, John!"
console.log(restored.createdAt instanceof Date); // true

Решение 3: reflect-metadata для декораторов

Используй TypeScript декораторы (требует experimentalDecorators):

import 'reflect-metadata';

type Constructor<T> = new (...args: any[]) => T;

function Serializable(target: Constructor<any>) {
  return target;
}

function SerializeAs(type: Constructor<any> | string) {
  return function (target: any, propertyKey: string) {
    Reflect.defineMetadata('custom:type', type, target, propertyKey);
  };
}

@Serializable
class User {
  name: string;

  @SerializeAs(Date)
  createdAt: Date;

  email: string;

  constructor(name: string, email: string, createdAt = new Date()) {
    this.name = name;
    this.email = email;
    this.createdAt = createdAt;
  }

  toJSON() {
    return {
      __type__: 'User',
      name: this.name,
      email: this.email,
      createdAt: this.createdAt.toISOString()
    };
  }

  static fromJSON(data: any) {
    const user = new User(data.name, data.email, new Date(data.createdAt));
    return user;
  }
}

Решение 4: JSON Schema с валидацией

Определи схему и валидируй данные:

interface UserData {
  __type__: 'User';
  name: string;
  email: string;
  createdAt: string; // ISO формат
}

class User {
  name: string;
  email: string;
  createdAt: Date;

  constructor(name: string, email: string, createdAt?: Date) {
    this.name = name;
    this.email = email;
    this.createdAt = createdAt || new Date();
  }

  static fromJSON(json: any): User {
    // Валидация
    if (json.__type__ !== 'User') {
      throw new TypeError('Invalid user type');
    }

    if (typeof json.name !== 'string') {
      throw new TypeError('name must be string');
    }

    if (typeof json.email !== 'string') {
      throw new TypeError('email must be string');
    }

    if (typeof json.createdAt !== 'string') {
      throw new TypeError('createdAt must be ISO string');
    }

    // Создание объекта
    const createdAt = new Date(json.createdAt);
    if (isNaN(createdAt.getTime())) {
      throw new TypeError('Invalid date format');
    }

    return new User(json.name, json.email, createdAt);
  }

  toJSON(): UserData {
    return {
      __type__: 'User',
      name: this.name,
      email: this.email,
      createdAt: this.createdAt.toISOString()
    };
  }
}

// Использование
const user = new User('John', 'john@example.com');
const json = JSON.stringify(user);
const restored = User.fromJSON(JSON.parse(json));

console.log(restored instanceof User); // true
console.log(restored.createdAt instanceof Date); // true

Решение 5: Готовые библиотеки

class-transformer (популярная библиотека):

import { plainToClass, Type } from 'class-transformer';

class User {
  name: string;

  @Type(() => Date)
  createdAt: Date;

  email: string;

  getGreeting() {
    return `Hello, ${this.name}!`;
  }
}

const plainObject = {
  name: 'John',
  email: 'john@example.com',
  createdAt: '2024-01-01T00:00:00.000Z'
};

const user = plainToClass(User, plainObject);
console.log(user instanceof User); // true
console.log(user.createdAt instanceof Date); // true
console.log(user.getGreeting()); // "Hello, John!"

Установка:

npm install class-transformer class-validator

Сравнение подходов

// 1. Простой (ручной)
const json = user.toJSON();
const restored = User.fromJSON(json);
// Плюсы: полный контроль, без зависимостей
// Минусы: много кода для каждого типа

// 2. Сериализатор с реестром типов
const json = Serializer.serialize(user);
const restored = Serializer.deserialize(json, User);
// Плюсы: универсальный для всех типов
// Минусы: нужна регистрация типов

// 3. Декораторы + reflect-metadata
@Serializable
class User { ... }
// Плюсы: элегантно, мало кода
// Минусы: нужно включать experimentalDecorators

// 4. class-transformer
const user = plainToClass(User, plainObject);
// Плюсы: мощно, хорошо документировано
// Минусы: зависимость, overhead

Практический пример: API клиент

class APIClient {
  async getUser(id: number): Promise<User> {
    const response = await fetch(`/api/users/${id}`);
    const data = await response.json();
    
    // Восстанавливаем объект с методами и типами
    return User.fromJSON(data);
  }

  async createUser(user: User): Promise<User> {
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(user.toJSON())
    });
    const data = await response.json();
    return User.fromJSON(data);
  }
}

// Использование
const api = new APIClient();
const user = await api.getUser(123);
console.log(user.getGreeting()); // Работает!
console.log(user.createdAt instanceof Date); // true

Заключение

Лучший подход зависит от ситуации:

  1. Простой проект — используй toJSON/fromJSON
  2. Много типов — создай Serializer с реестром
  3. Сложные структуры — try class-transformer
  4. Новый проект — используй TypeScript + декораторы

Всегда:

  • Добавляй __type__ метаданные в JSON
  • Конвертируй даты в ISO строки
  • Создай методы десериализации с валидацией
  • Тестируй восстановление объектов
Как восстановить переданный из одной TypeScript программы в другую объект в JSON чтобы сохранить метаданные? | PrepBro