Кастомные coercion функции

В библиотеке Superstruct coercion используется для преобразования входных данных перед этапом валидации. Это особенно важно при работе с внешними источниками данных:

  • HTTP-запросами;
  • формами;
  • query-параметрами;
  • переменными окружения;
  • JSON API;
  • пользовательским вводом.

Coercion позволяет:

  • автоматически преобразовывать строки в числа;
  • нормализовать даты;
  • обрабатывать null и undefined;
  • задавать значения по умолчанию;
  • очищать данные;
  • приводить данные к единому формату.

Основная функция для создания coercion-структур — coerce.


Сигнатура функции coerce

coerce(struct, condition, coercer)

Аргументы:

Аргумент Описание
struct итоговая структура после преобразования
condition структура-предикат для запуска coercion
coercer функция преобразования

Базовый пример coercion

import { coerce, number, string, create } fr om 'superstruct'

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

const result = create('42', NumberFromString)

console.log(result)

Результат:

42

Последовательность выполнения:

  1. Проверяется соответствие string();
  2. Вызывается coercion-функция;
  3. Выполняется преобразование;
  4. Проверяется итоговый number().

Отличие create от assert

Функция assert() только валидирует данные.

assert('42', NumberFromString)

Если coercion не запускается или результат невалиден — выбрасывается ошибка.

Функция create():

  • сначала запускает coercion;
  • затем возвращает преобразованное значение.

Поэтому coercion чаще всего используется именно вместе с create().


Преобразование строк в числа

Integer coercion

import { coerce, integer, string, create } from 'superstruct'

const IntFromString = coerce(
  integer(),
  string(),
  value => parseInt(value, 10)
)

console.log(create('100', IntFromString))

Float coercion

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

const FloatFromString = coerce(
  number(),
  string(),
  value => parseFloat(value)
)

console.log(create('10.55', FloatFromString))

Обработка NaN

Проблема:

Number('abc')

Вернёт:

NaN

Без дополнительной проверки это может привести к ошибкам.

Правильный вариант:

const SafeNumber = coerce(
  number(),
  string(),
  value => {
    const result = Number(value)

    if (Number.isNaN(result)) {
      return 0
    }

    return result
  }
)

Преобразование в boolean

Частая задача при работе с query-параметрами и .env.

const BooleanFromString = coerce(
  boolean(),
  string(),
  value => value === 'true'
)

Использование:

create('true', BooleanFromString)

Расширенный boolean coercion

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

const SmartBoolean = coerce(
  boolean(),
  string(),
  value => {
    const normalized = value.toLowerCase().trim()

    return [
      'true',
      '1',
      'yes',
      'on'
    ].includes(normalized)
  }
)

Преобразование timestamp в Date

import { coerce, date, number } from 'superstruct'

const DateFromTimestamp = coerce(
  date(),
  number(),
  value => new Date(value)
)

Преобразование строки в Date

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

const DateFromString = coerce(
  date(),
  string(),
  value => new Date(value)
)

Использование:

const result = create(
  '2025-01-01',
  DateFromString
)

Проверка валидности Date

Объект Date может быть невалидным.

new Date('wrong')

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

const SafeDate = coerce(
  date(),
  string(),
  value => {
    const result = new Date(value)

    if (Number.isNaN(result.getTime())) {
      return new Date(0)
    }

    return result
  }
)

Значения по умолчанию

Базовый default value

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

const DefaultName = coerce(
  string(),
  optional(string()),
  value => value ?? 'Anonymous'
)

Default values для объектов

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

const User = object({
  name: coerce(
    string(),
    string(),
    value => value.trim()
  )
})

Coercion внутри object

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

const User = object({
  age: coerce(
    number(),
    string(),
    value => Number(value)
  ),

  name: coerce(
    string(),
    string(),
    value => value.trim()
  )
})

Использование:

const result = create({
  age: '25',
  name: '  Alex  '
}, User)

console.log(result)

Результат:

{
  age: 25,
  name: 'Alex'
}

Нормализация строк

Trim coercion

const TrimmedString = coerce(
  string(),
  string(),
  value => value.trim()
)

Lowercase coercion

const LowercaseString = coerce(
  string(),
  string(),
  value => value.toLowerCase()
)

Uppercase coercion

const UppercaseString = coerce(
  string(),
  string(),
  value => value.toUpperCase()
)

Комбинирование coercion

const EmailStruct = coerce(
  string(),
  string(),
  value => value
    .trim()
    .toLowerCase()
)

Очистка телефонных номеров

const PhoneStruct = coerce(
  string(),
  string(),
  value => value.replace(/\D/g, '')
)

Пример:

create('+7 (777) 123-45-67', PhoneStruct)

Результат:

77771234567

Coercion массивов

Преобразование элементов массива

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

const NumberArray = coerce(
  array(number()),
  array(string()),
  values => values.map(Number)
)

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

const SafeNumberArray = coerce(
  array(number()),
  array(string()),
  values => {
    return values.map(value => {
      const num = Number(value)

      return Number.isNaN(num)
        ? 0
        : num
    })
  }
)

Coercion объектов целиком

Иногда требуется преобразовать структуру полностью.

const User = coerce(
  object({
    id: number(),
    name: string()
  }),

  object({
    id: string(),
    name: string()
  }),

  value => ({
    ...value,
    id: Number(value.id),
    name: value.name.trim()
  })
)

Coercion для query-параметров

Типичная задача в backend-разработке.

Исходные данные:

{
  page: '10',
  lim it: '20',
  active: 'true'
}

Преобразование:

import {
  object,
  number,
  boolean,
  string,
  coerce
} fr om 'superstruct'

const Query = object({
  page: coerce(
    number(),
    string(),
    Number
  ),

  lim it: coerce(
    number(),
    string(),
    Number
  ),

  active: coerce(
    boolean(),
    string(),
    value => value === 'true'
  )
})

Coercion переменных окружения

const Env = object({
  PORT: coerce(
    number(),
    string(),
    Number
  ),

  DEBUG: coerce(
    boolean(),
    string(),
    value => value === 'true'
  )
})

Использование refine вместе с coercion

Coercion и refine() отлично комбинируются.

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

const PositiveNumber = refine(
  coerce(
    number(),
    string(),
    Number
  ),

  'PositiveNumber',

  value => value > 0
)

Порядок выполнения

Важная особенность:

  1. Выполняется condition;
  2. Запускается coercion;
  3. Проверяется итоговая структура;
  4. Выполняются refine-валидаторы.

Многоступенчатое преобразование

const CleanNumber = coerce(
  number(),
  string(),
  value => {
    return Number(
      value
        .trim()
        .replace(',', '.')
    )
  }
)

Пример:

create(' 10,5 ', CleanNumber)

Результат:

10.5

Coercion nullable значений

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

const NullableString = coerce(
  nullable(string()),
  string(),
  value => {
    if (value === '') {
      return null
    }

    return value
  }
)

Coercion optional значений

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

const OptionalString = coerce(
  optional(string()),
  string(),
  value => {
    return value === ''
      ? undefined
      : value
  }
)

Универсальная factory-функция

Повторяющиеся coercion-структуры удобно выносить в фабрики.

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

function numberFromString() {
  return coerce(
    number(),
    string(),
    value => Number(value)
  )
}

Использование:

const User = object({
  age: numberFromString()
})

Generic coercion helper

function trimString() {
  return coerce(
    string(),
    string(),
    value => value.trim()
  )
}

Coercion и nested object

const User = object({
  profile: object({
    age: coerce(
      number(),
      string(),
      Number
    ),

    email: coerce(
      string(),
      string(),
      value => value.trim().toLowerCase()
    )
  })
})

Обработка пустых строк

Пустые строки часто требуют специальной логики.

const EmptyToNull = coerce(
  nullable(string()),
  string(),
  value => {
    return value.trim() === ''
      ? null
      : value
  }
)

Coercion enum-подобных значений

const RoleStruct = coerce(
  string(),
  string(),
  value => value.toLowerCase()
)

Sanitization данных

Coercion подходит для предварительной очистки данных.

const SafeHtml = coerce(
  string(),
  string(),
  value => value
    .replace(/<[^>]*>/g, '')
    .trim()
)

Частые ошибки

Использование assert вместо create

Неправильно:

assert(data, Struct)

Правильно:

create(data, Struct)

Отсутствие проверки NaN

Плохо:

value => Number(value)

Лучше:

value => {
  const result = Number(value)

  return Number.isNaN(result)
    ? 0
    : result
}

Изменение исходного объекта

Нежелательно:

value.id = Number(value.id)
return value

Лучше:

return {
  ...value,
  id: Number(value.id)
}

Производительность coercion

Coercion выполняется при каждой валидации.

Дорогие операции внутри coercion-функций могут существенно влиять на производительность:

value => heavyOperation(value)

Рекомендуется:

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

Архитектурные рекомендации

Выделение reusable coercion

export const Email = coerce(
  string(),
  string(),
  value => value.trim().toLowerCase()
)

Централизация transformations

export const transforms = {
  number: value => Number(value),

  boolean: value => value === 'true',

  trim: value => value.trim()
}

Композиция small coercion

Небольшие преобразования легче:

  • тестировать;
  • переиспользовать;
  • комбинировать;
  • поддерживать.

Плохо:

value => {
  // 100 строк логики
}

Лучше:

value => normalize(
  sanitize(
    transform(value)
  )
)