Библиотека 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
Здесь:
string()Number(value)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
}
)
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)
}
)
Иногда пустая строка должна превращаться в 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
Параметры 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
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)
}
)
const HexNumber = coerce(
number(),
string(),
value => parseInt(value, 16)
)
console.log(create('FF', HexNumber))
Результат:
255
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() вместе с coercionimport {
defaulted,
coerce,
number,
string
} from 'superstruct'
const Port = defaulted(
coerce(
number(),
string(),
value => Number(value)
),
3000
)
InfinityNumber('Infinity')
Результат:
Infinity
Если такие значения недопустимы:
const FiniteNumber = refine(
coerce(
number(),
string(),
value => Number(value)
),
'FiniteNumber',
value => Number.isFinite(value)
)
Superstruct не приводит автоматически bigint к
number.
Ручное преобразование:
const Numeric = coerce(
number(),
string(),
value => Number(BigInt(value))
)
Иногда 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(',', '.')
)
}
)
undefinedcreate(undefined, Numeric)
Проверка завершится ошибкой, потому что условие string()
не выполнится.
import {
optional,
number,
string,
coerce
} from 'superstruct'
const OptionalNumber = optional(
coerce(
number(),
string(),
value => Number(value)
)
)
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()
не допускает:
10.5
Даже если значение успешно преобразовалось через
Number().
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
}