Avatar ·

Как преобразовать JSON-объект в экземпляр класса TypeScript?

Всем привет! Я новичок в TypeScript и столкнулся с проблемой при работе с JSON-данными. У меня есть класс User с полями и методами: ```typescript class User { name: string; age: number; email: string; constructor(name: string, age: number, email: string) { this.name = name; this.age = age; this.email = email; } isAdult(): boolean { return this.age >= 18; } } ``` Когда я получаю данные с сервера в формате JSON, например через fetch: ```typescript const response = await fetch('/api/user'); const jsonData = await response.json(); // jsonData = { name: 'Иван', age: 25, email: 'ivan@example.com' } ``` Я пытаюсь привести его к типу User: ```typescript const user: User = jsonData as User; ``` Но потом, когда я вызываю метод user.isAdult(), TypeScript не выдаёт ошибку, но в рантайме я получаю "user.isAdult is not a function". Я понимаю, что as User это просто приведение типа для компилятора, но фактически объект не является экземпляром класса. Как правильно преобразовать JSON-объект в настоящий экземпляр класса со всеми методами? Есть ли какие-то стандартные подходы или библиотеки для этого? Буду благодарен за подробное объяснение с примерами! <<>>

Проблема приведения типов

Вы абсолютно правы: оператор as в TypeScript — это всего лишь утверждение типа для компилятора. Он не создаёт настоящий экземпляр класса, а только "говорит" TypeScript, что объект имеет такую форму. Методы класса при этом не копируются. JSON.parse() возвращает обычный объект без прототипа класса.

Правильное решение: ручное создание экземпляра

Самый надёжный способ — написать функцию или метод, который принимает сырой JSON и возвращает настоящий экземпляр класса:

class User {
  name: string;
  age: number;
  email: string;

  constructor(name: string, age: number, email: string) {
    this.name = name;
    this.age = age;
    this.email = email;
  }

  static fromJSON(json: any): User {
    return new User(json.name, json.age, json.email);
  }

  isAdult(): boolean {
    return this.age >= 18;
  }
}

// Использование:
const response = await fetch('/api/user');
const jsonData = await response.json();
const user = User.fromJSON(jsonData);
console.log(user.isAdult()); // true

Альтернатива: конструктор с объектом

Можно сделать конструктор, принимающий частичный объект:

class User {
  name: string;
  age: number;
  email: string;

  constructor(data: Partial<User>) {
    Object.assign(this, {
      name: '',
      age: 0,
      email: '',
      ...data
    });
  }

  isAdult(): boolean {
    return this.age >= 18;
  }

  static fromJSON(json: any): User {
    return new User(json);
  }
}

Использование библиотеки class-transformer

Для сложных проектов рекомендую библиотеку class-transformer от TypeStack:

import { plainToClass } from 'class-transformer';

class User {
  @Expose()
  name: string;

  @Expose()
  age: number;

  @Expose()
  email: string;

  get isAdult(): boolean {
    return this.age >= 18;
  }
}

// Использование:
const jsonData = { name: 'Иван', age: 25, email: 'ivan@example.com' };
const user = plainToClass(User, jsonData);
console.log(user.isAdult); // true

Важные моменты:

  • Всегда проверяйте данные — JSON может содержать невалидные значения. Используйте валидацию (например, class-validator).
  • Для вложенных объектов — рекурсивно создавайте экземпляры всех вложенных классов.
  • Используйте интерфейсы для типизации JSON — это отделяет "сырые" данные от бизнес-логики.

Почему это работает?

Когда вы вызываете new User(), JavaScript создаёт новый объект с прототипом User.prototype. Именно в прототипе хранятся методы класса. Простое приведение типа через as не меняет прототип объекта, поэтому методы недоступны.

Рекомендация для начинающих

Начните с простого статического метода fromJSON. Он понятен, не требует дополнительных зависимостей и даёт полный контроль над процессом преобразования. По мере роста проекта переходите на class-transformer — он автоматизирует рутину.

<<>>

Альтернативный подход: интерфейсы + функции-конструкторы

Многие разработчики предпочитают разделять данные и логику. Вместо класса используйте интерфейс для описания структуры данных и обычную функцию для валидации/преобразования:

interface UserData {
  name: string;
  age: number;
  email: string;
}

function createUser(data: UserData): UserData & { isAdult(): boolean } {
  if (!data.name || data.age < 0) {
    throw new Error('Invalid user data');
  }
  return {
    ...data,
    isAdult(): boolean {
      return this.age >= 18;
    }
  };
}

// Использование:
const jsonData = await fetch('/api/user').then(r => r.json());
const user = createUser(jsonData);
console.log(user.isAdult()); // true

Плюсы этого подхода:

  • Нет привязки к классам — проще тестировать и рефакторить.
  • Явная валидация — функция проверяет данные перед созданием объекта.
  • Гибкость — можно легко добавить вычисляемые свойства или методы.

Когда это уместно?

Если ваш проект не требует сложной иерархии классов или наследования, этот подход часто оказывается проще и чище. Он особенно хорош для API-слоя, где данные приходят с сервера и не должны содержать бизнес-логику.

Не забывайте, что в TypeScript главное — типизация, а не обязательно классы. Интерфейсы + чистые функции — это мощная идиома функционального программирования, которая отлично работает с JSON.