Валидация строк часто требует не только проверки типа, но и
нормализации значения. Одна из наиболее распространённых операций —
удаление пробелов в начале и конце строки. В JavaScript для этого
используется метод trim(), однако ручное применение в
каждом месте приводит к дублированию логики и ошибкам.
Библиотека Superstruct позволяет встроить очистку строк непосредственно в схему данных. Такой подход делает структуру данных самодостаточной: схема одновременно описывает тип, ограничения и правила преобразования.
Типичная пользовательская строка редко приходит в идеальном виде:
const username = ' admin '
Без обработки возникают проблемы:
Например:
'admin' !== ' admin '
При использовании Superstruct нормализация может выполняться автоматически во время валидации.
Простейшая строковая схема:
import { string } from 'superstruct'
const Username = string()
Проверка:
Username.is('alex') // true
Username.is(123) // false
Однако такая схема не изменяет данные. Строка с пробелами останется без изменений.
В Superstruct преобразование значений выполняется через
coerce.
Базовый синтаксис:
coerce(targetStruct, conditionStruct, transformer)
Параметры:
| Параметр | Назначение |
|---|---|
targetStruct |
итоговая структура |
conditionStruct |
условие применения |
transformer |
функция преобразования |
Наиболее распространённая реализация:
import { coerce, string } from 'superstruct'
const TrimmedString = coerce(
string(),
string(),
value => value.trim()
)
Теперь строка автоматически очищается:
TrimmedString.create(' hello ')
Результат:
'hello'
Метод create() выполняет:
Пример:
const result = TrimmedString.create(' test ')
console.log(result)
Результат:
test
Если значение не соответствует схеме:
TrimmedString.create(42)
Возникнет ошибка валидации.
Во многих примерах:
coerce(string(), string(), ...)
Обе структуры одинаковы.
Это означает:
Например:
const Trimmed = coerce(
string(),
string(),
value => value.trim()
)
Поведение:
| Значение | Результат |
|---|---|
' abc ' |
'abc' |
123 |
ошибка |
null |
ошибка |
Trimmed-строки почти всегда используются вместе с дополнительной проверкой.
Например, обязательное имя пользователя:
import {
coerce,
string,
size,
} from 'superstruct'
const Username = coerce(
size(string(), 3, 20),
string(),
value => value.trim()
)
Поведение:
Username.create(' alex ')
Результат:
'alex'
Проверка длины выполняется уже после trim().
Superstruct сначала выполняет coercion, затем validation.
Это особенно важно:
' ab '
После trim():
'ab'
Если минимальная длина равна 3:
size(string(), 3, 20)
валидация завершится ошибкой.
Практический пример:
import {
coerce,
pattern,
string,
} from 'superstruct'
const Email = coerce(
pattern(string(), /^[^\s@]+@[^\s@]+\.[^\s@]+$/),
string(),
value => value.trim()
)
Использование:
Email.create(' admin@example.com ')
Результат:
'admin@example.com'
Иногда поле необязательно:
import {
coerce,
optional,
string,
} from 'superstruct'
const OptionalTrimmed = optional(
coerce(
string(),
string(),
value => value.trim()
)
)
Поведение:
OptionalTrimmed.create(undefined)
Результат:
undefined
А строка:
OptionalTrimmed.create(' hello ')
превратится в:
'hello'
Nullable-поля допускают null.
import {
coerce,
nullable,
string,
} from 'superstruct'
const NullableTrimmed = nullable(
coerce(
string(),
string(),
value => value.trim()
)
)
Поведение:
NullableTrimmed.create(null)
Результат:
null
Частая задача в формах:
' '
После очистки:
''
Нередко требуется заменить это значение на
undefined.
Пример:
import {
coerce,
optional,
string,
unknown,
} from 'superstruct'
const EmptyToUndefined = coerce(
optional(string()),
unknown(),
value => {
if (typeof value !== 'string') {
return value
}
const trimmed = value.trim()
return trimmed === ''
? undefined
: trimmed
}
)
В предыдущих примерах условием было:
string()
Но теперь требуется принимать любые значения:
Поэтому используется:
unknown()
Преобразователь самостоятельно решает, что делать дальше.
Наиболее типичный сценарий — схемы объектов.
import {
object,
string,
coerce,
} from 'superstruct'
const TrimmedString = coerce(
string(),
string(),
value => value.trim()
)
const User = object({
username: TrimmedString,
email: TrimmedString,
})
Использование:
const user = User.create({
username: ' alex ',
email: ' alex@example.com ',
})
Результат:
{
username: 'alex',
email: 'alex@example.com'
}
Обычно trimmed-структуры выносятся отдельно:
// structs/trimmed.js
import {
coerce,
string,
} from 'superstruct'
export const TrimmedString = coerce(
string(),
string(),
value => value.trim()
)
После этого:
import { TrimmedString } from './structs/trimmed.js'
Базовую структуру удобно расширять.
import {
size,
} from 'superstruct'
const Username = size(
TrimmedString,
3,
20
)
const Title = size(
TrimmedString,
1,
120
)
const Comment = size(
TrimmedString,
1,
5000
)
В Superstruct можно комбинировать coercion с default-значениями.
Пример:
import {
defaulted,
} from 'superstruct'
const DisplayName = defaulted(
TrimmedString,
'Anonymous'
)
Поведение:
DisplayName.create(undefined)
Результат:
'Anonymous'
Массивы особенно выигрывают от автоматической очистки.
import {
array,
} from 'superstruct'
const Tags = array(TrimmedString)
Использование:
Tags.create([
' js ',
' node ',
' api ',
])
Результат:
['js', 'node', 'api']
Возможна комбинация нескольких типов.
import {
union,
number,
} from 'superstruct'
const Id = union([
TrimmedString,
number(),
])
refine добавляет дополнительную логику проверки.
Пример:
import {
refine,
} from 'superstruct'
const SafeUsername = refine(
TrimmedString,
'SafeUsername',
value => {
return !value.includes(' ')
}
)
Проверка:
SafeUsername.create(' admin ')
Успешно:
'admin'
Но:
SafeUsername.create(' admin user ')
вызовет ошибку.
Частое сочетание:
import {
pattern,
} from 'superstruct'
const Slug = pattern(
TrimmedString,
/^[a-z0-9-]+$/
)
Использование:
Slug.create(' my-post ')
Результат:
'my-post'
При большом количестве схем полезно создать фабрику.
import {
coerce,
string,
} from 'superstruct'
function trimmed(struct = string()) {
return coerce(
struct,
string(),
value => value.trim()
)
}
Использование:
const Username = trimmed(
size(string(), 3, 20)
)
Комбинация нескольких преобразований:
const LowercaseEmail = coerce(
string(),
string(),
value => value
.trim()
.toLowerCase()
)
Результат:
LowercaseEmail.create(' ADMIN@SITE.COM ')
Вернёт:
'admin@site.com'
Trim часто является только первым этапом.
const Phone = coerce(
string(),
string(),
value => {
return value
.trim()
.replace(/\s+/g, '')
}
)
Пример:
Phone.create(' +7 777 123 45 67 ')
Результат:
'+77771234567'
Типичная схема запроса:
const CreatePostRequest = object({
title: Title,
content: Comment,
tags: array(TrimmedString),
})
Входные данные:
{
title: ' Hello World ',
content: ' Text ',
tags: [' js ', ' node ']
}
После обработки:
{
title: 'Hello World',
content: 'Text',
tags: ['js', 'node']
}
Очистка через middleware:
req.body.name = req.body.name.trim()
имеет недостатки:
Подход Superstruct делает нормализацию частью контракта данных.
Метод:
assert(data, Struct)
не возвращает преобразованное значение.
Для coercion нужен именно:
create(data, Struct)
или:
Struct.create(...)
Неправильно:
size(string(), 3, 20)
а затем отдельно:
value.trim()
Проверка уже выполнена на исходной строке.
После trim:
' '
становится:
''
Это всё ещё строка. Если пустые значения запрещены, требуется дополнительная проверка.
Пример:
import {
refine,
} from 'superstruct'
const NonEmptyString = refine(
TrimmedString,
'NonEmptyString',
value => value.length > 0
)
Использование:
NonEmptyString.create(' ')
Завершится ошибкой.
Более компактный вариант:
const RequiredText = size(
TrimmedString,
1,
Infinity
)
После trim строка должна содержать минимум один символ.
Superstruct корректно выводит типы.
const Name = TrimmedString
Тип значения:
string
При использовании Infer:
import { Infer } from 'superstruct'
type Name = Infer<typeof TrimmedString>
Результат:
type Name = string
Часто создаётся набор базовых примитивов:
export const TrimmedString
export const NonEmptyString
export const Email
export const Username
export const Slug
export const Title
После этого все схемы строятся только из готовых компонентов.
Пример:
const Article = object({
title: Title,
slug: Slug,
author: Username,
})
Такой подход: