Coerce и transform

Преобразование входных данных в структурированную форму является одной из ключевых задач при работе с внешними источниками информации: API, формами, файлами конфигурации. В библиотеке Superstruct эта задача решается через механизмы принудительного приведения типов и трансформации значений, которые позволяют не только проверять данные, но и нормализовать их в нужный формат ещё до попадания в бизнес-логику.

В стандартной модели валидации Superstruct входное значение либо соответствует структуре, либо нет. Однако в реальных сценариях данные часто приходят в «грязном» виде: числа в виде строк, даты в текстовом формате, булевы значения как "true" и "false".

Механизм coercion (приведения типов) решает проблему на уровне схемы, позволяя автоматически преобразовывать входные значения перед проверкой.

Ключевая идея:

coerce выполняется до валидации, transform может выполняться как до, так и после, в зависимости от композиции структуры.


Механизм coerce

Функция coerce позволяет задать правило преобразования значения, если оно соответствует определённому типу или условию.

Общая форма:

coerce(structure, (value) => transformedValue)

Пример приведения строки к числу:

import { coerce, number, string, validate } from 'superstruct'

const NumberFromString = coerce(number(), string(), (value) => {
  return Number(value)
})

validate(NumberFromString, "42")
// => [null, 42]

Здесь происходит следующая цепочка:

  1. Входное значение "42" соответствует строке
  2. Срабатывает coercion
  3. Строка преобразуется в число 42
  4. Выполняется проверка типа number

Если бы преобразование не произошло, значение "42" было бы отклонено.


Coerce как фильтр типов входных данных

Coerce часто используется для обработки «широких» входных типов.

Пример: обработка числа, которое может прийти как строка или как число:

import { coerce, number, string, union } from 'superstruct'

const LooseNumber = coerce(number(), union([number(), string()]), (value) => {
  return typeof value === 'string' ? Number(value) : value
})

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


Приведение булевых значений

Частый кейс — преобразование "true" и "false" в boolean:

import { coerce, boolean, string } from 'superstruct'

const BooleanFromString = coerce(boolean(), string(), (value) => {
  if (value === 'true') return true
  if (value === 'false') return false
  return value
})

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


transform как инструмент преобразования

В отличие от coerce, функция transform применяется к уже валидированному значению. Это позволяет изменять данные после проверки структуры.

Базовая форма:

transform(structure, (value) => newValue)

Пример преобразования строки в Date:

import { string, transform } from 'superstruct'

const DateFromString = transform(string(), (value) => {
  return new Date(value)
})

Такой подход гарантирует, что на вход поступает корректная строка, а на выходе всегда объект Date.


Различие coerce и transform

Разделение этих механизмов принципиально:

  • coerce — изменение входных данных до проверки
  • transform — изменение уже проверенных данных

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

Пример:

const A = coerce(number(), string(), Number)

Если передать "abc", результат будет NaN, но структура уже считает значение числом, и дальнейшая валидация может пройти некорректно.

С transform ситуация иная:

const B = transform(string(), (v) => new Date(v))

Здесь гарантируется, что вход — строка, а результат всегда Date, но отсутствие проверки корректности даты должно контролироваться отдельно.


Комбинирование coerce и transform

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

Пример: строка → число → округление:

import { coerce, number, string, transform } from 'superstruct'

const RoundedNumber = transform(
  coerce(number(), string(), Number),
  (value) => Math.round(value)
)

Цепочка выглядит так:

  1. Строка преобразуется в число
  2. Выполняется проверка number
  3. Применяется округление

Обработка сложных структур

Coerce и transform применяются не только к примитивам, но и к объектам.

Пример нормализации объекта конфигурации:

import { object, string, number, coerce } from 'superstruct'

const Config = object({
  port: coerce(number(), string(), Number),
  host: string(),
})

Вход:

{
  port: "8080",
  host: "localhost"
}

После обработки:

{
  port: 8080,
  host: "localhost"
}

Coerce с массивами

Часто данные приходят в виде строки с разделителями:

"1,2,3"

Преобразование в массив чисел:

import { coerce, array, number, string } from 'superstruct'

const NumberArray = coerce(array(number()), string(), (value) => {
  return value.split(',').map(Number)
})

Такая схема широко используется при работе с query-параметрами URL.


Защитные ограничения при преобразованиях

Несмотря на гибкость, coercion может скрывать ошибки данных. Например:

Number("abc") // NaN

Если такой результат проходит дальше, структура теряет смысл.

Поэтому часто добавляется дополнительная проверка:

import { coerce, number, string, refine } from 'superstruct'

const SafeNumber = coerce(
  refine(number(), 'finite', (v) => Number.isFinite(v)),
  string(),
  (value) => Number(value)
)

Такой подход исключает некорректные значения.


Вложенные трансформации

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

import { object, string, transform } from 'superstruct'

const User = object({
  createdAt: transform(string(), (v) => new Date(v)),
  name: string(),
})

Каждое поле может иметь собственную стратегию преобразования.


Преобразование с нормализацией данных

Одним из распространённых сценариев является нормализация входных данных в единый формат.

Пример: приведение строк к нижнему регистру:

import { string, transform } from 'superstruct'

const NormalizedString = transform(string(), (value) => {
  return value.trim().toLowerCase()
})

Это особенно полезно при работе с поисковыми запросами и идентификаторами.


Многоступенчатая обработка данных

В сложных схемах данные проходят несколько этапов:

  1. Coerce (приведение типов)
  2. Validate (проверка структуры)
  3. Transform (финальная нормализация)

Пример:

const Schema = transform(
  coerce(number(), string(), Number),
  (value) => value * 2
)

Хотя здесь используется упрощённая цепочка, логика масштабируется на более сложные структуры.


Практические особенности применения

При проектировании схем важно учитывать:

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

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


Работа с API и внешними источниками

Наиболее частое применение coercion и transform — обработка данных API:

  • строки вместо чисел
  • null вместо отсутствующих значений
  • даты в ISO-строке
  • JSON внутри строки

Пример:

const ApiResponse = object({
  id: coerce(number(), string(), Number),
  createdAt: transform(string(), (v) => new Date(v)),
  payload: transform(string(), JSON.parse),
})

Такая схема позволяет из «сырого» ответа получить структурированный объект без промежуточной обработки в бизнес-логике.