Символы и уникальные значения

Природа символов как уникальных значений

Символ (Symbol) в JavaScript представляет собой примитивный тип данных, создающий гарантированно уникальные значения. Даже при одинаковом описании каждый вызов Symbol() возвращает новое значение, не равное никакому другому символу.

const a = Symbol("id");
const b = Symbol("id");

a === b; // false

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

Существует также глобальный реестр символов через Symbol.for, где значения повторно используются:

const a = Symbol.for("shared");
const b = Symbol.for("shared");

a === b; // true

Эта двойственность (локальные и глобальные символы) напрямую влияет на моделирование данных в схемах валидации.


Поддержка символов в Zod

Библиотека Zod предоставляет примитив z.symbol() для описания значений типа Symbol.

import { z } from "zod";

const SymbolSchema = z.symbol();

Данный тип проверяет, что значение действительно является JavaScript Symbol:

SymbolSchema.parse(Symbol("ok")); // успешно
SymbolSchema.parse("not symbol");  // ошибка

При этом Zod не ограничивает символ конкретным значением — проверяется только тип.


Уникальные символы как единичные значения

В реальных схемах часто требуется не просто «любой символ», а строго определённый уникальный маркер. Это важно при моделировании:

  • внутренних идентификаторов состояний
  • дискриминаторов в union-типах
  • скрытых маркеров протоколов
  • системных флагов

Для этого используется комбинация Symbol() и z.literal().

Литералы символов

Zod позволяет использовать z.literal для строгого сравнения значений, включая символы:

const UNIQUE = Symbol("unique");

const Schema = z.literal(UNIQUE);

Schema.parse(UNIQUE); // корректно
Schema.parse(Symbol("unique")); // ошибка

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


Глобальные символы и стабильные идентификаторы

При использовании Symbol.for появляется возможность создавать стабильные значения между модулями или даже разными частями приложения.

const TOKEN = Symbol.for("auth.token");

const Schema = z.literal(TOKEN);

Такой подход применяется в системах, где требуется гарантированное совпадение идентификатора без экспорта констант.

Однако следует учитывать: глобальный реестр делает символы разделяемыми, что снижает степень их «уникальности» как маркеров.


Символы в объединениях и дискриминации типов

Символы могут использоваться как дискриминаторы, хотя чаще применяются строки. В Zod это реализуется через z.discriminatedUnion, но ключ дискриминации должен быть сравнимым значением.

Пример с символами:

const A = Symbol("A");
const B = Symbol("B");

const SchemaA = z.object({
  kind: z.literal(A),
  value: z.number()
});

const SchemaB = z.object({
  kind: z.literal(B),
  value: z.string()
});

const Union = z.union([SchemaA, SchemaB]);

Такой подход создаёт строгую типизацию состояний, где невозможно случайное совпадение строковых идентификаторов.


Ограничения использования символов

Несмотря на выразительность, символы имеют ряд ограничений в контексте сериализации и передачи данных:

  • не сериализуются в JSON
  • теряются при сохранении состояния
  • сложны для логирования и отладки без контекста
  • не поддерживаются во внешних API
JSON.stringify({ key: Symbol("x") }); // {}

Это делает их пригодными только для внутренних структур и runtime-логики.


Моделирование уникальных значений через литералы

Помимо символов, Zod предоставляет более универсальный механизм — z.literal, который поддерживает:

  • строки
  • числа
  • булевы значения
  • null и undefined
  • символы
const A = z.literal("A");
const B = z.literal(1);
const C = z.literal(Symbol.for("C"));

Литералы часто применяются как более сериализуемая альтернатива символам, особенно в API-схемах.


Брендированные типы как усиление уникальности

В Zod существует механизм брендинга, позволяющий создавать логически уникальные типы поверх примитивов. Это особенно полезно там, где символы не подходят из-за проблем сериализации.

const UserId = z.string().brand<"UserId">();

const id = UserId.parse("123");

Бренд не влияет на runtime-значение, но добавляет строгую типизацию на уровне TypeScript.

Символы в этом контексте выступают как runtime-аналог брендинга, тогда как brand() — как compile-time механизм.


Сравнение символов, литералов и брендов

Символы

  • гарантированная уникальность
  • невозможность сериализации
  • сильная защита от коллизий

Литералы

  • поддержка сериализации
  • простота использования
  • отсутствие истинной уникальности при одинаковых значениях

Бренды

  • отсутствие runtime-накладных расходов
  • усиление типизации
  • не влияют на реальные данные

Использование символов как скрытых ключей

В некоторых архитектурах символы применяются для создания невидимых свойств объектов, которые не участвуют в стандартных операциях.

const INTERNAL = Symbol("internal");

const schema = z.object({
  name: z.string()
}).transform((data) => {
  data[INTERNAL] = true;
  return data;
});

Zod при этом не различает «видимые» и «скрытые» ключи — символы просто проходят как обычные значения, если они описаны схемой.


Глобальные паттерны уникальных значений

При проектировании систем часто комбинируются разные подходы:

  • символы для runtime-идентификаторов
  • литералы для API-совместимых значений
  • бренды для TypeScript-уровня безопасности
const STATE_IDLE = Symbol.for("state.idle");
const STATE_LOADING = Symbol.for("state.loading");

const StateSchema = z.union([
  z.literal(STATE_IDLE),
  z.literal(STATE_LOADING)
]);

Такой подход создаёт строгую модель состояния, устойчивую к случайным совпадениям строк.


Особенности сравнения и валидации

Zod выполняет проверку символов через строгую идентичность:

  • для z.symbol() — проверка typeof value === "symbol"
  • для z.literal(symbol) — проверка по ссылке
  • для union — последовательная проверка всех схем

Это делает символы максимально предсказуемыми в рамках runtime-валидации, но требует аккуратного управления их созданием и экспортом между модулями.