Введение: Миф о «магическом» приведении типов
Каждый TypeScript-разработчик рано или поздно сталкивается с ситуацией: вы получаете JSON с сервера, уверены, что знаете его структуру, и делаете простой cast через as. И… ничего не работает. Методы класса отсутствуют, поля undefined, а дебаг превращается в квест.
Вопрос «How do I cast a JSON object to a TypeScript class?» — один из самых популярных на Stack Overflow (десятки тысяч просмотров). И неудивительно. TypeScript — это надстройка, которая существует только во время компиляции. В runtime ваш «класс» — это просто plain JavaScript object.
В этой статье мы разберем 5 самых частых и опасных ошибок, которые совершают разработчики при работе с JSON и классами TypeScript. Вы узнаете, как их избежать, и получите готовые решения для production-кода.
Ошибка №1: Использование as для «настоящего» приведения
Самая распространенная ловушка — думать, что конструкция jsonData as MyClass создает экземпляр класса.
Что происходит на самом деле?
Оператор as (или angle-bracket синтаксис) — это type assertion. Он говорит компилятору: «Доверься мне, я знаю тип». Но в JavaScript он не выполняет никакой магии.
class User {
name: string = '';
greet(): string {
return `Привет, ${this.name}!`;
}
}
const rawData = JSON.parse('{"name": "Анна"}');
const user = rawData as User;
console.log(user.name); // "Анна" — работает
console.log(user.greet()); // TypeError: user.greet is not a function
Почему? Потому что rawData — это обычный объект { name: 'Анна' }. У него нет прототипа User.prototype, а значит, и метода greet.
Как исправить?
Всегда создавайте настоящий экземпляр класса. Базовый способ — Object.assign:
const user = Object.assign(new User(), rawData);
console.log(user.greet()); // "Привет, Анна!"
Но у этого метода есть нюансы, о которых мы поговорим дальше.
Ошибка №2: Игнорирование вложенных объектов и массивов
Вы радостно используете Object.assign, но ваши данные содержат вложенные структуры. И тут начинается ад.
class Address {
city: string = '';
zip: string = '';
}
class Company {
name: string = '';
address: Address = new Address();
}
const data = JSON.parse('{"name": "TechCorp", "address": {"city": "Москва", "zip": "101000"}}');
const company = Object.assign(new Company(), data);
console.log(company.address.city); // "Москва" — повезло?
console.log(company.address instanceof Address); // false!
Проблема: address остался простым объектом, а не экземпляром Address. Если у Address будут методы или computed properties, они потеряются.
Решение: Рекурсивное приведение
Пишем функцию-фабрику или используем библиотеку class-transformer:
import { plainToClass } from 'class-transformer';
const company = plainToClass(Company, data);
console.log(company.address instanceof Address); // true
class-transformer рекурсивно обходит все поля и создает правильные экземпляры классов на основе декораторов @Type.
Ошибка №3: Потеря значений по умолчанию
TypeScript позволяет задавать значения полей по умолчанию прямо в классе. Но при «касте» JSON эти значения могут быть перезаписаны undefined.
class Config {
theme: string = 'light';
fontSize: number = 14;
language: string = 'ru';
}
const partialData = JSON.parse('{"theme": "dark"}');
const config = Object.assign(new Config(), partialData);
console.log(config.fontSize); // 14 — ок
console.log(config.language); // 'ru' — ок
Кажется, всё работает? Но что если в JSON придет поле со значением null?
const badData = JSON.parse('{"theme": "dark", "fontSize": null}');
const config = Object.assign(new Config(), badData);
console.log(config.fontSize); // null — а ожидали 14!
Как защититься?
Вариант 1: Фильтровать null перед assign:
function cleanAndAssign<T>(cls: new () => T, data: Partial<T>): T {
const instance = new cls();
Object.keys(data).forEach(key => {
if (data[key as keyof T] !== null && data[key as keyof T] !== undefined) {
(instance as any)[key] = data[key as keyof T];
}
});
return instance;
}
Вариант 2 (профессиональный): Использовать class-validator + class-transformer для валидации и преобразования.
Ошибка №4: Забываем про даты и специальные типы
JSON не поддерживает тип Date. Все даты приходят как строки. Если вы ожидаете объект Date, то получите обычную строку.
class Event {
title: string = '';
date: Date = new Date();
}
const eventData = JSON.parse('{"title": "Встреча", "date": "2024-12-01T10:00:00Z"}');
const event = Object.assign(new Event(), eventData);
console.log(typeof event.date); // "string" — а не "object"
console.log(event.date instanceof Date); // false
Решение: Явное преобразование
Самый надежный способ — написать метод fromJSON внутри класса:
class Event {
title: string = '';
date: Date = new Date();
static fromJSON(data: any): Event {
const event = new Event();
event.title = data.title;
event.date = new Date(data.date);
return event;
}
}
const event = Event.fromJSON(eventData);
console.log(event.date instanceof Date); // true
Если используете class-transformer, добавьте декоратор @Type(() => Date).
Ошибка №5: Наивная вера в типы из сторонних API
Вы получили JSON от API, который «гарантированно» возвращает определённую структуру. Но реальность сурова: поля могут отсутствовать, типы могут не совпадать, а структура — меняться.
interface ApiResponse {
id: number;
name: string;
email: string;
}
// Вдруг API вернул:
const badResponse = JSON.parse('{"id": "123", "name": null, "email": "test@test"}');
// id — строка, name — null. Ваш код упадет.
Стратегия защиты:
- Всегда валидируйте входящие данные с помощью библиотек типа
zodилиio-ts. - Используйте стр