Числовые преобразования

Библиотека Superstruct поддерживает не только проверку типов, но и преобразование данных. Числовые преобразования особенно важны при работе с HTTP-запросами, параметрами URL, формами, CSV-файлами и внешними API, где числа часто приходят в виде строк.

Механизм преобразования строится вокруг функции coerce(), позволяющей автоматически модифицировать входные значения перед основной валидацией.


Функция coerce()

Сигнатура:

coerce(struct, condition, transformer)

Параметры:

Параметр Назначение
struct Итоговая структура после преобразования
condition Проверка, когда запускать преобразование
transformer Функция преобразования

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

Частая ситуация — получение числа в виде строки:

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

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

const result = create('42', Numeric)

console.log(result)

Результат:

42

Здесь:

  1. Проверяется условие string()
  2. Если значение является строкой — запускается Number(value)
  3. Результат проходит финальную проверку number()

Почему create() используется чаще assert()

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

assert('42', Numeric)

Но преобразование не возвращается наружу.

Для получения итогового значения применяется create():

const value = create('42', Numeric)

Именно create() создаёт новый объект или значение с учётом coercion.


Преобразование целых чисел

Строки можно конвертировать в integer:

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

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

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

Результат:

100

Разница между Number() и parseInt()

Number()

Number('10.5')

Результат:

10.5

parseInt()

parseInt('10.5', 10)

Результат:

10

Особенности parseInt()

parseInt('100px', 10)

Результат:

100

А Number('100px') вернёт:

NaN

Безопасное преобразование чисел

Прямая конвертация может приводить к NaN.

Небезопасный вариант

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

Проблема:

Number('abc')

Результат:

NaN

Хотя NaN формально имеет тип number.


Проверка NaN

Для защиты используется Number.isNaN():

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

    if (Number.isNaN(result)) {
      throw new Error('Invalid number')
    }

    return result
  }
)

Преобразование float-значений

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

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

console.log(create('12.75', FloatValue))

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

Пустые строки — распространённый источник ошибок.

Поведение Number('')

Number('')

Результат:

0

Это может быть нежелательным.


Игнорирование пустых значений

const Numeric = coerce(
  number(),
  string(),
  value => {
    if (value.trim() === '') {
      throw new Error('Empty string')
    }

    return Number(value)
  }
)

Nullable-числа

Иногда пустая строка должна превращаться в null.

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

const NullableNumber = coerce(
  nullable(number()),
  string(),
  value => {
    if (value.trim() === '') {
      return null
    }

    return Number(value)
  }
)

console.log(create('', NullableNumber))

Результат:

null

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

Параметры URL всегда приходят строками.

Пример

const query = {
  page: '5',
  lim it: '20'
}

Создание структуры:

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

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

const QueryStruct = object({
  page: Numeric,
  lim it: Numeric
})

const result = create(query, QueryStruct)

console.log(result)

Результат:

{
  page: 5,
  lim it: 20
}

Множественные преобразования

Допускается последовательная нормализация.

const Numeric = coerce(
  number(),
  string(),
  value => Number(value.trim())
)

Удаление пробелов

console.log(create('   42   ', Numeric))

Результат:

42

Работа с scientific notation

JavaScript поддерживает экспоненциальную запись:

Number('1e3')

Результат:

1000

Superstruct корректно работает с такими значениями:

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

console.log(create('1e3', Numeric))

Ограничение диапазона чисел

После coercion можно использовать refinement.

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

const PositiveNumber = refine(
  coerce(
    number(),
    string(),
    value => Number(value)
  ),
  'PositiveNumber',
  value => value > 0
)

Преобразование с округлением

Округление вниз

const Rounded = coerce(
  number(),
  string(),
  value => Math.floor(Number(value))
)

Округление вверх

const Rounded = coerce(
  number(),
  string(),
  value => Math.ceil(Number(value))
)

Классическое округление

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

Преобразование денежных значений

Строка:

'199.99'

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

const Price = coerce(
  number(),
  string(),
  value => {
    const normalized = value.replace(',', '.')

    return Number(normalized)
  }
)

Работа с локализованными числами

Некоторые системы отправляют:

'1 500,75'

Нормализация:

const LocalizedNumber = coerce(
  number(),
  string(),
  value => {
    const normalized = value
      .replace(/\s/g, '')
      .replace(',', '.')

    return Number(normalized)
  }
)

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

const HexNumber = coerce(
  number(),
  string(),
  value => parseInt(value, 16)
)

console.log(create('FF', HexNumber))

Результат:

255

Преобразование binary-значений

const BinaryNumber = coerce(
  number(),
  string(),
  value => parseInt(value, 2)
)

console.log(create('1010', BinaryNumber))

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

Массив строк:

['1', '2', '3']

Структура:

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

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

const Numbers = array(Numeric)

console.log(create(['1', '2'], Numbers))

Результат:

[1, 2]

Обработка некорректных элементов массива

create(['1', 'abc'], Numbers)

Возникнет ошибка валидации.


Преобразование вложенных объектов

const Config = object({
  server: object({
    port: coerce(
      number(),
      string(),
      value => Number(value)
    )
  })
})

Использование defaulted() вместе с coercion

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

const Port = defaulted(
  coerce(
    number(),
    string(),
    value => Number(value)
  ),
  3000
)

Особенности Infinity

Number('Infinity')

Результат:

Infinity

Если такие значения недопустимы:

const FiniteNumber = refine(
  coerce(
    number(),
    string(),
    value => Number(value)
  ),
  'FiniteNumber',
  value => Number.isFinite(value)
)

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

Superstruct не приводит автоматически bigint к number.

Ручное преобразование:

const Numeric = coerce(
  number(),
  string(),
  value => Number(BigInt(value))
)

Использование union для смешанных типов

Иногда API отправляет либо число, либо строку.

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

const FlexibleNumber = coerce(
  number(),
  union([string(), number()]),
  value => Number(value)
)

Нормализация перед преобразованием

Типичный pipeline:

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

Поведение при undefined

create(undefined, Numeric)

Проверка завершится ошибкой, потому что условие string() не выполнится.


Поддержка optional-значений

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

const OptionalNumber = optional(
  coerce(
    number(),
    string(),
    value => Number(value)
  )
)

Создание переиспользуемого coercion

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

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

export function numeric() {
  return coerce(
    number(),
    string(),
    value => {
      const result = Number(value)

      if (Number.isNaN(result)) {
        throw new Error('Invalid number')
      }

      return result
    }
  )
}

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

const User = object({
  age: numeric(),
  score: numeric()
})

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

coerce() вызывается для каждого значения отдельно.

При обработке больших массивов:

array(Numeric)

функция преобразования запускается для каждого элемента массива.

Сложные регулярные выражения и многоступенчатая нормализация могут влиять на производительность при обработке больших объёмов данных.


Типичные ошибки

Использование parseInt() без radix

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

parseInt(value)

Правильно:

parseInt(value, 10)

Игнорирование NaN

Опасный код:

value => Number(value)

Без проверки могут проходить невалидные значения.


Смешивание integer и float

integer()

не допускает:

10.5

Даже если значение успешно преобразовалось через Number().


Преобразование пустой строки в 0

Number('')

Результат:

0

Такое поведение часто приводит к труднообнаружимым ошибкам.


Практический пример конфигурации

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

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

    if (Number.isNaN(result)) {
      throw new Error('Invalid number')
    }

    return result
  }
)

const Config = object({
  host: string(),
  port: Numeric,
  timeout: Numeric,
  retries: Numeric
})

const raw = {
  host: 'localhost',
  port: '3000',
  timeout: '5000',
  retries: '3'
}

const config = create(raw, Config)

console.log(config)

Результат:

{
  host: 'localhost',
  port: 3000,
  timeout: 5000,
  retries: 3
}