27 / 36
Как типизировать JSON-значение?
Полный ответ
Наивный подход и почему он плохой:
TypeScript
// ❌ any — ноль типобезопасности
function parse(raw: string): any {
return JSON.parse(raw);
}
// ❌ unknown — безопасно, но неудобно, нужен каст на каждом шаге
function parse2(raw: string): unknown {
return JSON.parse(raw);
}
Правильный рекурсивный тип:
TypeScript
// Точное описание всех допустимых JSON-значений
type JsonPrimitive = string | number | boolean | null;
type JsonArray = JsonValue[];
type JsonObject = { [key: string]: JsonValue };
type JsonValue = JsonPrimitive | JsonArray | JsonObject;
Использование:
TypeScript
// Типобезопасный парсинг
function parseJson(raw: string): JsonValue {
return JSON.parse(raw) as JsonValue;
}
const data = parseJson('{"name": "Alice", "scores": [95, 87]}');
// TypeScript знает, что это JsonValue
if (typeof data === "object" && data !== null && !Array.isArray(data)) {
const name = data["name"]; // JsonValue
if (typeof name === "string") {
console.log(name.toUpperCase()); // безопасно
}
}
Типизация API-ответа:
TypeScript
// Конкретная структура, совместимая с JSON
interface ApiResponse {
status: "ok" | "error";
data: JsonValue;
timestamp: number;
}
// Проверяем, что интерфейс действительно JSON-совместим
type AssertJsonCompatible<T extends JsonValue> = T;
type Check = AssertJsonCompatible<ApiResponse>; // ОК
// А вот это не скомпилируется:
interface BadResponse {
created: Date; // Date не сериализуется
handler: () => void; // функции нельзя
}
// type Fail = AssertJsonCompatible<BadResponse>; // ошибка компиляции
Типизация JSON с известной схемой:
TypeScript
// Когда знаете структуру — используйте конкретный тип
interface UserConfig {
theme: "light" | "dark";
fontSize: number;
shortcuts: Record<string, string>;
recent: string[];
}
function loadConfig(raw: string): UserConfig {
const parsed: JsonValue = JSON.parse(raw);
// Валидация через Zod, io-ts или ручную проверку
return parsed as UserConfig; // после валидации
}
Generic-обёртка для JSON-совместимых типов:
TypeScript
// Утилита: извлечь только JSON-совместимые свойства
type JsonCompatible<T> = {
[K in keyof T]: T[K] extends JsonValue
? T[K]
: T[K] extends object
? JsonCompatible<T[K]>
: never;
};
interface User {
id: number;
name: string;
created: Date; // не JSON
greet: () => string; // не JSON
}
type JsonUser = JsonCompatible<User>;
// { id: number; name: string; created: never; greet: never }
Реальные кейсы
Безопасная работа с localStorage:
TypeScript
function getFromStorage(key: string): JsonValue | undefined {
const raw = localStorage.getItem(key);
if (raw === null) return undefined;
return JSON.parse(raw) as JsonValue;
}
function setToStorage(key: string, value: JsonValue): void {
localStorage.setItem(key, JSON.stringify(value));
}
setToStorage("prefs", { theme: "dark", fontSize: 14 }); // OK
// setToStorage("fn", () => {}); // ошибка: функция не JsonValue
Ошибка: забыть, что undefined не является JSON-значением:
TypeScript
// ❌ undefined тихо исчезает при сериализации
const obj = { a: 1, b: undefined };
JSON.stringify(obj); // '{"a":1}' — поле b пропало
// ✅ Используйте null для явного «пусто»
const obj2 = { a: 1, b: null };
JSON.stringify(obj2); // '{"a":1,"b":null}'
Резюме
JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue } — рекурсивный тип, покрывающий весь JSON. Он защищает от случайной сериализации Date, undefined, функций и классов. Используйте его для типизации generic API, хранилищ и межсервисного обмена.