Введение: JSON и TypeScript — идеальная пара для работы с данными
JSON (JavaScript Object Notation) — это легковесный формат обмена данными, который стал стандартом де-факто для взаимодействия между клиентом и сервером, а также для хранения конфигураций и передачи структурированной информации. TypeScript, будучи строго типизированным надмножеством JavaScript, предоставляет мощные инструменты для безопасной и предсказуемой работы с JSON. В этой статье мы на практике разберём, как правильно парсить (преобразовывать строку JSON в объект) и сериализовать (преобразовывать объект в строку JSON) данные в TypeScript, избегая типичных ошибок и используя возможности системы типов.
Основы: JSON.parse() и JSON.stringify() в TypeScript
TypeScript полностью наследует встроенные методы JavaScript для работы с JSON: JSON.parse() и JSON.stringify(). Однако их использование в строго типизированной среде требует особого подхода.
JSON.parse() — из строки в объект
Метод JSON.parse() принимает строку JSON и возвращает объект. В TypeScript результат по умолчанию имеет тип any, что лишает нас всех преимуществ типизации. Рассмотрим пример:
const jsonString = '{"name": "TypeScript", "version": 5.3}';
const parsed: any = JSON.parse(jsonString);
console.log(parsed.name); // Работает, но нет автодополнения и проверки типов
Чтобы получить типизированный результат, используйте приведение типов (type assertion):
interface Language {
name: string;
version: number;
}
const jsonString = '{"name": "TypeScript", "version": 5.3}';
const parsed = JSON.parse(jsonString) as Language;
console.log(parsed.name.toUpperCase()); // Автодополнение работает!
JSON.stringify() — из объекта в строку
JSON.stringify() сериализует объект в строку JSON. TypeScript проверяет, что переданный объект является валидным для сериализации (не содержит функций, undefined, Symbol и т.д.).
interface User {
id: number;
name: string;
isActive: boolean;
}
const user: User = { id: 1, name: "Alice", isActive: true };
const jsonString = JSON.stringify(user);
console.log(jsonString); // '{"id":1,"name":"Alice","isActive":true}'
Продвинутые техники: типизация и обработка ошибок
На практике работа с JSON редко бывает идеальной. Данные могут прийти с сервера в неожиданном формате, содержать лишние поля или отсутствовать обязательные. Рассмотрим, как TypeScript помогает справляться с этими проблемами.
Создание обобщённой функции для безопасного парсинга
Напишем универсальную функцию, которая принимает строку JSON и возвращает типизированный результат с обработкой ошибок:
function safeParse<T>(json: string): T | null {
try {
return JSON.parse(json) as T;
} catch (error) {
console.error("Ошибка парсинга JSON:", error);
return null;
}
}
// Использование
interface Product {
id: number;
title: string;
price: number;
}
const rawData = '{"id": 101, "title": "Laptop", "price": 999.99}';
const product = safeParse<Product>(rawData);
if (product) {
console.log(product.title); // "Laptop"
}
Валидация данных с помощью пользовательских охранников типа
Даже после приведения типа as T TypeScript не проверяет, что данные действительно соответствуют интерфейсу. Для runtime-валидации используйте type guards:
function isProduct(obj: any): obj is Product {
return (
typeof obj === 'object' &&
obj !== null &&
typeof obj.id === 'number' &&
typeof obj.title === 'string' &&
typeof obj.price === 'number'
);
}
const rawData = '{"id": "abc", "title": "Phone", "price": 599}';
const parsed = JSON.parse(rawData);
if (isProduct(parsed)) {
console.log(parsed.price.toFixed(2)); // Безопасно
} else {
console.error("Некорректный формат данных");
}
Работа со сложными структурами: вложенные объекты и массивы
Реальные данные часто содержат вложенные объекты и массивы. TypeScript позволяет точно описать такие структуры.
Парсинг массива объектов
interface Comment {
id: number;
text: string;
author: string;
}
interface Post {
id: number;
title: string;
comments: Comment[];
}
const jsonString = `{
"id": 1,
"title": "TypeScript Guide",
"comments": [
{"id": 101, "text": "Great article!", "author": "Bob"},
{"id": 102, "text": "Very useful", "author": "Jane"}
]
}`;
const post = JSON.parse(jsonString) as Post;
post.comments.forEach(comment => {
console.log(`${comment.author}: ${comment.text}`);
});
Сериализация с форматированием и фильтрацией
JSON.stringify() принимает дополнительные параметры: replacer (функция или массив для фильтрации полей) и space (количество пробелов для отступов).
interface Config {
apiKey: string;
baseUrl: string;
timeout: number;
debug: boolean;
}
const config: Config = {
apiKey: "secret-123",
baseUrl: "https://api.example.com",
timeout: 5000,
debug: false
};
// Исключаем apiKey из сериализации
const safeConfig = JSON.stringify(config, ["baseUrl", "timeout", "debug"], 2);
console.log(safeConfig);
/* Вывод:
{
"baseUrl": "https://api.example.com",
"timeout": 5000,
"debug": false
}
*/
Практический пример: интеграция с REST API
Рассмотрим реальный сценарий — получение данных с сервера и их обработка. Используем fetch и типизацию.
interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}
async function fetchTodos(): Promise<Todo[]> {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: Todo[] = await response.json() as Todo[];
return data;
} catch (error) {
console.error("Ошибка загрузки задач:", error);
return [];
}
}
// Использование
(async () => {
const todos = await fetchTodos();
const incompleteTodos = todos.filter(todo => !todo.completed);
console.log(`Невыполненных задач: ${incompleteTodos.length}`);
})();