Потери типов при сериализации в localStorage

Хранилище localStorage работает исключительно со строками. Любое значение, которое сохраняется туда, автоматически приводится к строковому виду. Это фундаментальное ограничение определяет поведение всей системы хранения и является основной причиной потери типов данных при работе с ним.

При сохранении объектов, чисел, булевых значений или сложных структур разработчик неизбежно сталкивается с необходимостью сериализации. На практике почти всегда используется JSON.stringify, а при чтении — JSON.parse. Однако даже эта пара инструментов не решает проблему полностью, поскольку JSON не поддерживает полный набор типов JavaScript.


Механизм преобразования значений в localStorage

При записи данных происходит последовательность преобразований:

  1. Значение приводится к строке.
  2. Строка сохраняется в виде пары ключ-значение.
  3. При чтении возвращается только строка.

Пример:

localStorage.setItem("value", 42);
localStorage.getItem("value"); // "42"

Число 42 автоматически становится строкой "42". Уже на этом этапе теряется исходный тип.

При использовании объектов процесс выглядит иначе:

const user = { name: "Alex", age: 30 };

localStorage.setItem("user", JSON.stringify(user));
const result = JSON.parse(localStorage.getItem("user"));

В этом случае структура сохраняется, но типовая информация — нет.


Потеря базовых типов при JSON-сериализации

JSON поддерживает ограниченный набор типов:

  • строки
  • числа
  • логические значения
  • массивы
  • объекты
  • null

Все остальные типы приводятся к одному из этих представлений или теряются.

undefined

JSON.stringify({ a: undefined }); // "{}"

Ключ полностью исчезает из результата. Это приводит к неоднозначности: отсутствие поля может означать как реальное отсутствие данных, так и явное значение undefined.


Function

JSON.stringify({
  fn: function () { return 1; }
}); // "{}"

Функции полностью исключаются из сериализации. При восстановлении данных информация теряется без возможности восстановления.


Symbol

JSON.stringify({ id: Symbol("x") }); // "{}"

Символы также игнорируются, так как не имеют JSON-представления.


Частичная поддержка сложных типов

Некоторые встроенные типы преобразуются, но теряют семантику.

Date

const obj = { date: new Date() };
const json = JSON.stringify(obj);

Результат:

{"date":"2026-01-24T10:00:00.000Z"}

После восстановления:

const parsed = JSON.parse(json);
typeof parsed.date; // string

Дата превращается в строку, и для возврата исходного типа требуется дополнительная обработка:

parsed.date = new Date(parsed.date);

NaN, Infinity и -Infinity

JSON.stringify({ a: NaN, b: Infinity });
// {"a":null,"b":null}

Эти значения приводятся к null, что делает невозможным их различение после восстановления.


BigInt

JSON.stringify({ id: 10n });
// TypeError: Do not know how to serialize a BigInt

BigInt не поддерживается JSON вовсе и требует ручной сериализации:

JSON.stringify({ id: "10n" });

Структуры Map, Set, WeakMap, WeakSet

Map

const map = new Map([["a", 1]]);
JSON.stringify(map); // "{}"

Map превращается в пустой объект, так как его внутренний формат не совместим с JSON.

Set

const set = new Set([1, 2, 3]);
JSON.stringify(set); // "{}"

Аналогично, структура теряется.

Для восстановления требуется ручная сериализация:

const serialized = JSON.stringify([...set]);
const restored = new Set(JSON.parse(serialized));

WeakMap и WeakSet

Эти структуры вообще не сериализуются, поскольку не предоставляют возможности перечисления элементов.


Потеря прототипов и классов

При сериализации экземпляров классов сохраняются только поля объекта:

class User {
  constructor(name) {
    this.name = name;
  }
  greet() {
    return `Hi ${this.name}`;
  }
}

const user = new User("Alex");
const json = JSON.stringify(user);
const parsed = JSON.parse(json);

После восстановления:

  • теряется связь с User.prototype
  • методы становятся недоступны
parsed.greet; // undefined

Для восстановления требуется ручная реконструкция:

Object.setPrototypeOf(parsed, User.prototype);

Потеря точности при числах и строках

Хотя JSON корректно хранит числа, возникают пограничные случаи:

  • очень большие числа теряют точность
  • дробные значения могут округляться при дальнейшей обработке
  • строки, содержащие числа, могут быть ошибочно интерпретированы
JSON.stringify({ big: 99999999999999999 });
// "100000000000000000"

Проблема неоднозначности типов

Основная проблема localStorage и JSON — отсутствие типовой информации. После восстановления невозможно отличить:

{
  value: null
}

от:

{
  value: undefined
}

или от отсутствующего поля:

{}

Все три случая могут привести к одному и тому же результату при неаккуратной обработке.


Реконструкция типов при помощи reviver

JSON.parse позволяет использовать функцию восстановления:

JSON.parse(text, (key, value) => {
  if (typeof value === "string" && value.endsWith("Z")) {
    return new Date(value);
  }
  return value;
});

Это частично решает проблему, но требует:

  • строгого соглашения о форматах
  • дополнительной логики
  • контроля всех возможных типов

Пользовательская сериализация как способ сохранения типов

Для минимизации потерь используют расширенные схемы сериализации:

const serialize = (value) => ({
  __type: typeof value,
  value
});

Пример:

const data = {
  date: serialize(new Date().toISOString()),
  set: serialize([...new Set([1, 2])])
};

Однако такой подход быстро усложняется при росте количества типов.


Ограничения localStorage как фактор архитектуры

Потери типов — не просто техническая деталь, а архитектурное ограничение:

  • отсутствие бинарных форматов хранения
  • невозможность кастомных сериализаторов на уровне API
  • синхронный доступ к данным
  • ограничение только строковым форматом

Это приводит к тому, что любое сложное состояние приложения требует дополнительного слоя абстракции.


Типовые ошибки при работе с сериализацией

Ошибка перезаписи типов

localStorage.setItem("count", "10");
const count = localStorage.getItem("count") + 1; // "101"

Ошибка проверки существования

if (localStorage.getItem("flag")) {
  // true даже если "false"
}

Ошибка сравнения

localStorage.setItem("a", false);
localStorage.getItem("a") === false; // false

Косвенные эффекты потери типов

Потеря типизации приводит к более глубоким последствиям:

  • усложнение бизнес-логики
  • необходимость постоянного приведения типов
  • рост количества runtime-ошибок
  • увеличение объёма защитного кода

В результате работа с localStorage в сложных приложениях превращается в ручное управление сериализацией на уровне приложения.


Практические подходы к снижению потерь

Часто применяются следующие стратегии:

  • явное хранение метаданных типа
  • использование обёрток над localStorage
  • централизованная сериализация и десериализация
  • использование альтернативных хранилищ с поддержкой структурированных данных

Такие подходы позволяют частично компенсировать фундаментальные ограничения строкового хранилища и JSON-модели данных.