Дополнительные проверки

Функция 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)
    )
  }
)

Проверка email

const Email = refine(string(), 'Email', value => {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
})

Проверка URL

const Url = refine(string(), 'Url', value => {
  try {
    new URL(value)
    return true
  } catch {
    return false
  }
})

Проверка UUID

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
)

Проверка JSON-строки

const JsonString = refine(
  string(),
  'JsonString',
  value => {
    try {
      JSON.parse(value)
      return true
    } catch {
      return false
    }
  }
)

Проверка значения с помощью is

is возвращает булево значение и удобна для условной логики.

import { is } from 'superstruct'

if (is(data, User)) {
  console.log('Корректный объект')
}

Проверка через validate

validate возвращает кортеж с ошибкой и значением.

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
  }
)

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


Ограничения refine

refine подходит для:

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

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)
  }
)

Проверка slug

const Slug = refine(
  string(),
  'Slug',
  value => {
    return /^[a-z0-9-]+$/.test(value)
  }
)

Проверка HEX-цвета

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
  }
)