← Назад к вопросам
Как восстановить переданный из одной 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
Заключение
Лучший подход зависит от ситуации:
- Простой проект — используй toJSON/fromJSON
- Много типов — создай Serializer с реестром
- Сложные структуры — try class-transformer
- Новый проект — используй TypeScript + декораторы
Всегда:
- Добавляй
__type__метаданные в JSON - Конвертируй даты в ISO строки
- Создай методы десериализации с валидацией
- Тестируй восстановление объектов