Avatar ·

Как инициализировать объект TypeScript данными из JSON?

Всем привет! Я только начал изучать TypeScript и столкнулся с проблемой, которая кажется простой, но я не могу найти элегантное решение. У меня есть API, который возвращает JSON с данными пользователя. Вот пример того, что приходит с сервера: ```typescript const jsonData = { "id": 42, "name": "Иван Петров", "email": "ivan@example.com", "isActive": true, "createdAt": "2024-03-15T10:30:00Z" }; ``` Я создал интерфейс для этого объекта: ```typescript interface User { id: number; name: string; email: string; isActive: boolean; createdAt: Date; } ``` Проблема в том, что когда я пытаюсь просто присвоить JSON объекту типа User: ```typescript const user: User = jsonData; // Ошибка! createdAt - строка, а не Date ``` TypeScript ругается, потому что `createdAt` в JSON — это строка, а в интерфейсе я указал тип `Date`. Я понимаю, что могу просто использовать `any`, но это плохая практика. Как правильно инициализировать объект TypeScript из JSON, особенно когда типы не совпадают напрямую (например, строки дат нужно преобразовать в объекты Date)? Нужен подход, который будет работать для больших вложенных объектов и при этом сохранит типизацию. Буду благодарен за примеры кода! <<>>

Проблема: JSON не знает о типах TypeScript

Вы столкнулись с фундаментальной особенностью: JSON — это текстовый формат, который содержит только примитивные типы (строки, числа, булевы значения, null, массивы и объекты). В нём нет дат, классов или пользовательских типов. Поэтому createdAt приходит как строка, а не как объект Date. Простое приведение типа через as User не сработает — это лишь подсказка компилятору, без реального преобразования данных.

Лучшее решение: фабричная функция с валидацией

Самый надёжный способ — написать функцию, которая принимает любой объект, проверяет его структуру и преобразует поля в нужные типы. Это даёт полный контроль и сохраняет типизацию.

interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
  createdAt: Date;
}

function createUser(data: unknown): User {
  // Проверяем, что data — объект и не null
  if (typeof data !== 'object' || data === null) {
    throw new Error('Invalid user data: expected an object');
  }

  // Используем Record для доступа к полям
  const obj = data as Record<string, unknown>;

  // Проверяем каждое поле
  if (typeof obj.id !== 'number') {
    throw new Error('Invalid user data: id must be a number');
  }
  if (typeof obj.name !== 'string') {
    throw new Error('Invalid user data: name must be a string');
  }
  if (typeof obj.email !== 'string') {
    throw new Error('Invalid user data: email must be a string');
  }
  if (typeof obj.isActive !== 'boolean') {
    throw new Error('Invalid user data: isActive must be a boolean');
  }
  if (typeof obj.createdAt !== 'string') {
    throw new Error('Invalid user data: createdAt must be a string');
  }

  // Преобразуем строку даты в объект Date
  const date = new Date(obj.createdAt);
  if (isNaN(date.getTime())) {
    throw new Error('Invalid user data: createdAt is not a valid date string');
  }

  // Возвращаем готовый объект
  return {
    id: obj.id,
    name: obj.name,
    email: obj.email,
    isActive: obj.isActive,
    createdAt: date,
  };
}

// Использование:
const jsonData = {
  id: 42,
  name: "Иван Петров",
  email: "ivan@example.com",
  isActive: true,
  createdAt: "2024-03-15T10:30:00Z"
};

const user = createUser(jsonData);
console.log(user.createdAt instanceof Date); // true
console.log(user.createdAt.toISOString()); // работает!

Альтернатива: библиотека Zod для валидации схем

Если у вас много сложных объектов, ручная проверка становится утомительной. Используйте библиотеку Zod — она позволяет объявить схему и автоматически проверять/преобразовывать данные.

import { z } from 'zod';

// Определяем схему
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  isActive: z.boolean(),
  createdAt: z.string().transform((str) => new Date(str)),
});

// Тип выводится автоматически
type User = z.infer<typeof UserSchema>;

// Парсим JSON
const jsonData = {
  id: 42,
  name: "Иван Петров",
  email: "ivan@example.com",
  isActive: true,
  createdAt: "2024-03-15T10:30:00Z"
};

const user = UserSchema.parse(jsonData);
console.log(user.createdAt instanceof Date); // true

Что выбрать?

  • Фабричная функция — если проект небольшой, вы хотите минимум зависимостей и полный контроль.
  • Zod — для больших проектов с множеством API, где важна автоматическая валидация и читаемость.
  • Классы с конструктором — если вам нужны методы объекта (но помните: JSON не содержит методы, их нужно добавлять отдельно).

Главное правило: никогда не доверяйте данным из JSON. Всегда проверяйте их структуру и типы, особенно если данные приходят из внешнего источника.

<<>>

Использование классов с конструктором

Если вам нужны не только данные, но и методы, можно создать класс, который принимает частичные данные и дополняет их:

class User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
  createdAt: Date;

  constructor(data: Partial<User>) {
    this.id = data.id ?? 0;
    this.name = data.name ?? '';
    this.email = data.email ?? '';
    this.isActive = data.isActive ?? false;
    this.createdAt = data.createdAt instanceof Date 
      ? data.createdAt 
      : new Date(data.createdAt ?? '');
  }

  getFormattedDate(): string {
    return this.createdAt.toLocaleDateString('ru-RU');
  }
}

// Использование:
const jsonData = {
  id: 42,
  name: "Иван Петров",
  email: "ivan@example.com",
  isActive: true,
  createdAt: "2024-03-15T10:30:00Z"
};

const user = new User(jsonData);
console.log(user.getFormattedDate()); // "15.03.2024"

Когда это полезно?

  • Если объект должен иметь поведение (методы).
  • Если нужно установить значения по умолчанию для отсутствующих полей.
  • Если вы используете DI-контейнеры или фреймворки, ожидающие классы.

Недостаток: нужно вручную писать конструктор для каждого класса. Для больших проектов лучше комбинировать с Zod или фабричной функцией из первого ответа.