Динамические ключи

Работа с динамическими ключами возникает в структурах данных, где заранее неизвестен полный набор свойств объекта. Такие случаи типичны для конфигураций, словарей, маппингов, ответов API и пользовательских настроек. В Superstruct эта задача решается через специализированные структуры, позволяющие описывать «словари» с произвольными ключами и строго типизированными значениями.

Базовая концепция динамических ключей

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

Superstruct предоставляет примитив для таких сценариев — record, который описывает структуру вида:

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

Классический пример — словарь числовых значений:

import { record, string, number } from 'superstruct';

const Scores = record(string(), number());

Такая структура означает: ключи — строки, значения — числа.


Record как основа динамических структур

record является ключевым инструментом для работы с динамическими ключами. Он принимает два аргумента:

  1. Структура ключа
  2. Структура значения

Пример с более строгими ограничениями:

import { record, string, integer } from 'superstruct';

const AgeMap = record(string(), integer());

Здесь формируется отображение имени пользователя на возраст, где:

  • ключи — строки (имена)
  • значения — целые числа

Любая попытка передать значение другого типа приведёт к ошибке валидации.


Ограничение ключей через структуру

Хотя JavaScript допускает практически любые строковые ключи, Superstruct позволяет ограничить допустимые ключи через дополнительные структуры.

Пример ограничения ключей через форматирование строки:

import { record, pattern, number } from 'superstruct';

const HexValues = record(pattern(/[a-f0-9]{6}/i), number());

Здесь ключами могут быть только строки, соответствующие шестнадцатеричному шаблону.

Такой подход часто применяется для:

  • цветовых таблиц
  • кодов ошибок
  • идентификаторов сущностей фиксированного формата

Динамические ключи с вложенными структурами

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

import { record, string, object, number } from 'superstruct';

const UserMap = record(
  string(),
  object({
    age: number(),
    score: number(),
  })
);

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


Использование record для конфигураций

Одним из частых применений динамических ключей являются конфигурационные объекты:

import { record, string, boolean } from 'superstruct';

const FeatureFlags = record(string(), boolean());

Такая структура позволяет включать и отключать функциональность по ключу:

{
  darkMode: true,
  betaUI: false,
  analytics: true
}

При этом Superstruct гарантирует, что:

  • ключи являются строками
  • значения строго boolean

Сочетание с union для значений

Динамические ключи часто требуют гибких типов значений. В таких случаях используется union.

import { record, string, union, number, boolean } from 'superstruct';

const FlexibleMap = record(
  string(),
  union([number(), boolean()])
);

Теперь значения могут быть либо числом, либо булевым типом.

Это полезно в ситуациях, где данные приходят из внешних источников с неоднородной структурой.


Глубокие динамические структуры

В сложных системах значения сами могут содержать динамические ключи:

import { record, string, number } from 'superstruct';

const Matrix = record(
  string(),
  record(string(), number())
);

Это структура двумерной карты, где:

  • внешний ключ — строка (например, имя строки)
  • внутренний ключ — строка (например, столбец)
  • значение — число

Такая модель используется для:

  • матриц расстояний
  • графов смежности
  • таблиц пересечений

Частичное применение динамических ключей

В некоторых случаях необходимо сочетать фиксированные поля с динамическими ключами. Superstruct позволяет комбинировать object и record.

import { object, string, number, record } from 'superstruct';

const Settings = object({
  userId: string(),
  preferences: record(string(), number()),
});

Здесь структура содержит:

  • фиксированное поле userId
  • динамический словарь preferences

Валидация пустых и отсутствующих ключей

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

const EmptyAllowed = record(string(), number());

Допустимыми будут:

{}

Если требуется запрет пустых структур, применяется дополнительная логика через refine:

import { record, string, number, refine } from 'superstruct';

const NonEmptyRecord = refine(
  record(string(), number()),
  'NonEmptyRecord',
  value => Object.keys(value).length > 0
);

Динамические ключи и преобразование данных

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

import { record, string, number, coerce } from 'superstruct';

const CoercedMap = coerce(
  record(string(), number()),
  record(string(), string()),
  value => {
    const result = {};
    for (const key in value) {
      result[key] = Number(value[key]);
    }
    return result;
  }
);

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


Использование динамических ключей в типизации API-ответов

Многие REST и GraphQL API возвращают данные в виде словарей с неизвестными заранее ключами.

const ApiResponse = record(string(), object({
  id: string(),
  value: number(),
}));

Это позволяет описывать ответы следующего вида:

{
  itemA: { id: "itemA", value: 10 },
  itemB: { id: "itemB", value: 25 }
}

Ограничения и особенности record

Несмотря на универсальность, динамические структуры имеют особенности:

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

Для более сложной логики применяется комбинация refine, validate и пользовательских функций.


Сочетание record с partial и optional логикой

Хотя record уже предполагает динамичность, его можно комбинировать с частичными структурами для значений:

import { record, string, object, optional, number } from 'superstruct';

const Data = record(
  string(),
  object({
    value: optional(number()),
  })
);

Теперь каждое значение может либо содержать value, либо не содержать его вовсе.


Практическая модель использования динамических ключей

Наиболее типичный сценарий — хранение коллекций сущностей:

const Entities = record(
  string(),
  object({
    name: string(),
    rating: number(),
    active: boolean(),
  })
);

Такая структура представляет собой индексированную коллекцию, где ключ — идентификатор, а значение — полная модель объекта.


Итоговая модель мышления

Динамические ключи в Superstruct строятся вокруг идеи обобщённого словаря, в котором:

  • структура ключей задаётся отдельно
  • структура значений строго типизирована
  • ограничения могут быть расширены через дополнительные композиции

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