Функция refine добавляет дополнительную логику поверх
уже существующего типа. Базовая структура проверяет форму данных, а
refine позволяет внедрять произвольные условия.
import { object, string, refine } from 'superstruct'
const Email = refine(string(), 'Email', value => {
return value.includes('@')
})
const User = object({
email: Email
})
Проверка:
import { assert } from 'superstruct'
assert(
{ email: 'admin@example.com' },
User
)
Ошибка:
assert(
{ email: 'invalid-email' },
User
)
import { string, refine } from 'superstruct'
const Password = refine(string(), 'Password', value => {
return value.length >= 8
})
Комбинирование нескольких условий:
const StrongPassword = refine(string(), 'StrongPassword', value => {
return (
value.length >= 8 &&
/[A-Z]/.test(value) &&
/[0-9]/.test(value)
)
})
import { number, refine } from 'superstruct'
const Age = refine(number(), 'Age', value => {
return value >= 18 && value <= 65
})
Использование:
import { is } from 'superstruct'
is(25, Age) // true
is(10, Age) // false
Дополнительная логика часто применяется к коллекциям.
import { array, number, refine } from 'superstruct'
const Scores = refine(
array(number()),
'Scores',
values => values.length >= 3
)
Проверка уникальности:
const UniqueNumbers = refine(
array(number()),
'UniqueNumbers',
values => {
return new Set(values).size === values.length
}
)
refine может анализировать сразу несколько полей.
import { object, string, refine } from 'superstruct'
const RegisterForm = refine(
object({
password: string(),
confirmPassword: string()
}),
'RegisterForm',
value => {
return value.password === value.confirmPassword
}
)
import { string, refine } from 'superstruct'
const ISODate = refine(string(), 'ISODate', value => {
return /^\d{4}-\d{2}-\d{2}$/.test(value)
})
Более строгая версия:
const StrictISODate = refine(
string(),
'StrictISODate',
value => {
const date = new Date(value)
return (
!Number.isNaN(date.getTime()) &&
/^\d{4}-\d{2}-\d{2}$/.test(value)
)
}
)
const Email = refine(string(), 'Email', value => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
})
const Url = refine(string(), 'Url', value => {
try {
new URL(value)
return true
} catch {
return false
}
})
const UUID = refine(string(), 'UUID', value => {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
.test(value)
})
Хотя Superstruct содержит enums, дополнительные проверки
позволяют строить более сложные ограничения.
import { string, refine } from 'superstruct'
const Role = refine(string(), 'Role', value => {
return ['admin', 'user', 'moderator'].includes(value)
})
Проверка может зависеть от нескольких параметров.
import { object, number, boolean, refine } from 'superstruct'
const Product = refine(
object({
price: number(),
discount: number(),
active: boolean()
}),
'Product',
value => {
if (!value.active) {
return true
}
return value.discount < value.price
}
)
refine работает синхронно. Для асинхронной валидации
обычно выполняют дополнительный этап после проверки структуры.
Пример:
import { object, string, assert } from 'superstruct'
const UserSchema = object({
login: string()
})
async function validateUser(data) {
assert(data, UserSchema)
const exists = await checkLogin(data.login)
if (exists) {
throw new Error('Логин уже существует')
}
return data
}
Выделение проверок в отдельные функции упрощает поддержку.
function minLength(length) {
return value => value.length >= length
}
Использование:
const Username = refine(
string(),
'Username',
minLength(5)
)
Комбинирование:
function hasUppercase(value) {
return /[A-Z]/.test(value)
}
function hasDigit(value) {
return /[0-9]/.test(value)
}
const Password = refine(
string(),
'Password',
value => {
return (
minLength(8)(value) &&
hasUppercase(value) &&
hasDigit(value)
)
}
)
import {
object,
string,
array,
refine
} from 'superstruct'
const Tag = refine(string(), 'Tag', value => {
return value.length >= 2
})
const Article = object({
title: string(),
tags: array(Tag)
})
createФункция create не только валидирует данные, но и
возвращает итоговый объект.
import {
object,
string,
create
} from 'superstruct'
const User = object({
name: string()
})
const user = create(
{ name: 'Alex' },
User
)
console.log(user)
Совместно с дополнительными проверками:
const NonEmptyString = refine(
string(),
'NonEmptyString',
value => value.trim().length > 0
)
const PositiveNumber = refine(
number(),
'PositiveNumber',
value => value > 0
)
Проверка целых чисел:
const Integer = refine(
number(),
'Integer',
Number.isInteger
)
Проверка чётности:
const EvenNumber = refine(
number(),
'EvenNumber',
value => value % 2 === 0
)
Superstruct особенно полезен для проверки доменных ограничений.
const Transfer = refine(
object({
from: string(),
to: string(),
amount: number()
}),
'Transfer',
value => {
return (
value.from !== value.to &&
value.amount > 0
)
}
)
const Event = refine(
object({
start: string(),
end: string()
}),
'Event',
value => {
return (
new Date(value.start) <
new Date(value.end)
)
}
)
function regexStruct(pattern, name = 'Regex') {
return refine(
string(),
name,
value => pattern.test(value)
)
}
Использование:
const HexColor = regexStruct(
/^#([0-9A-F]{3}){1,2}$/i,
'HexColor'
)
const NonEmptyArray = refine(
array(string()),
'NonEmptyArray',
value => value.length > 0
)
const JsonString = refine(
string(),
'JsonString',
value => {
try {
JSON.parse(value)
return true
} catch {
return false
}
}
)
isis возвращает булево значение и удобна для условной
логики.
import { is } from 'superstruct'
if (is(data, User)) {
console.log('Корректный объект')
}
validatevalidate возвращает кортеж с ошибкой и значением.
import { validate } from 'superstruct'
const [error, value] = validate(
'admin@example.com',
Email
)
if (error) {
console.error(error)
}
Дополнительные проверки должны быть понятными и предсказуемыми.
Плохо:
const Value = refine(
string(),
'Value',
value => value.length > 5
)
Лучше:
const Username = refine(
string(),
'Username',
value => {
return value.length >= 6
}
)
Название структуры отображается в сообщениях об ошибках, поэтому оно должно отражать смысл ограничения.
refinerefine подходит для:
refine не предназначен для:
Для преобразований обычно используется отдельный этап нормализации данных.
Проверки должны быть быстрыми и детерминированными.
Неудачный пример:
const SlowStruct = refine(
string(),
'SlowStruct',
value => {
const start = Date.now()
while (Date.now() - start < 1000) {}
return true
}
)
Оптимальный подход:
const FastStruct = refine(
string(),
'FastStruct',
value => value.length > 0
)
Проверки можно строить слоями.
const NonEmptyString = refine(
string(),
'NonEmptyString',
value => value.trim().length > 0
)
const ShortString = refine(
NonEmptyString,
'ShortString',
value => value.length <= 20
)
const Phone = refine(
string(),
'Phone',
value => {
return /^\+?[0-9]{10,15}$/.test(value)
}
)
const Slug = refine(
string(),
'Slug',
value => {
return /^[a-z0-9-]+$/.test(value)
}
)
const HexColor = refine(
string(),
'HexColor',
value => {
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
.test(value)
}
)
const CardNumber = refine(
string(),
'CardNumber',
value => {
return /^[0-9]{16}$/.test(value)
}
)
const Config = refine(
object({
host: string(),
port: number()
}),
'Config',
value => {
return value.port > 0 &&
value.port < 65536
}
)
function minItems(count) {
return refine(
array(string()),
'MinItems',
value => value.length >= count
)
}
const Tags = minItems(2)
const UniqueUsers = refine(
array(
object({
id: number(),
name: string()
})
),
'UniqueUsers',
values => {
const ids = values.map(v => v.id)
return new Set(ids).size === ids.length
}
)