Автоматическое приведение типов

Одной из ключевых возможностей Superstruct является автоматическое преобразование входных данных к ожидаемому формату. Эта функциональность особенно полезна при работе с HTTP-запросами, параметрами форм, JSON-ответами внешних API и пользовательским вводом, где типы данных часто не соответствуют ожидаемой структуре.

Вместо ручного преобразования строк в числа, дат в объекты Date, а булевых значений из "true" в true, библиотека позволяет централизованно описать правила преобразования и валидации.


Отличие валидации от приведения типов

Стандартная валидация проверяет уже существующее значение:

import { number, assert } from 'superstruct'

assert(42, number())

Если передать строку:

assert('42', number())

возникнет ошибка:

Expected a number, but received: "42"

Автоматическое приведение типов решает эту проблему: значение сначала преобразуется, затем проходит валидацию.


Функция create

Главным механизмом автоматического преобразования является функция create.

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

Сигнатура:

create(value, struct)

Она:

  1. Выполняет преобразование значений
  2. Проверяет результат
  3. Возвращает готовый объект

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

Пример без преобразования

import { assert, number } from 'superstruct'

assert('25', number())

Ошибка:

Expected a number, but received: "25"

Пример с coerce

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

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

const result = create('25', MyNumber)

console.log(result)

Результат:

25

Функция coerce

Функция coerce создаёт структуру с преобразованием.

Сигнатура:

coerce(TargetStruct, ConditionStruct, transformer)

Аргументы

Аргумент Назначение
TargetStruct Целевая структура
ConditionStruct Тип, при котором выполняется преобразование
transformer Функция преобразования

Логика работы coerce

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

Алгоритм:

  1. Проверяется, соответствует ли значение string()
  2. Если да — вызывается transformer
  3. Полученный результат валидируется как number()

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

Целые числа

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

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

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

Числа с плавающей точкой

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

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

console.log(create('12.55', FloatFromString))

Преобразование булевых значений

HTTP-запросы и формы часто передают булевы значения как строки.

Пример

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

const BooleanFromString = coerce(
  boolean(),
  string(),
  value => value === 'true'
)

console.log(create('true', BooleanFromString))
console.log(create('false', BooleanFromString))

Результат:

true
false

Универсальный boolean-парсер

const BooleanStruct = coerce(
  boolean(),
  string(),
  value => {
    const normalized = value.toLowerCase()

    return [
      'true',
      '1',
      'yes',
      'on'
    ].includes(normalized)
  }
)

Поддерживаемые значения:

true
TRUE
1
yes
on

Преобразование строк в Date

Базовый пример

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

const DateFromString = coerce(
  date(),
  string(),
  value => new Date(value)
)

const result = create(
  '2025-01-10',
  DateFromString
)

console.log(result)

Проблема невалидных дат

new Date() может вернуть некорректный объект.

new Date('invalid')

Результат:

Invalid Date

При этом объект всё равно имеет тип Date.


Безопасная проверка даты

import {
  define,
  object,
  string,
  coerce,
  create
} from 'superstruct'

const ValidDate = define('ValidDate', value => {
  return (
    value instanceof Date &&
    !Number.isNaN(value.getTime())
  )
})

const DateStruct = coerce(
  ValidDate,
  string(),
  value => new Date(value)
)

console.log(
  create('2025-02-20', DateStruct)
)

Автоматическое преобразование объектов

Пример структуры пользователя

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

const User = object({
  id: coerce(
    number(),
    string(),
    value => Number(value)
  ),

  age: coerce(
    number(),
    string(),
    value => Number(value)
  ),

  isAdmin: coerce(
    boolean(),
    string(),
    value => value === 'true'
  ),

  name: string()
})

const result = create({
  id: '10',
  age: '25',
  isAdmin: 'true',
  name: 'Alex'
}, User)

console.log(result)

Результат:

{
  id: 10,
  age: 25,
  isAdmin: true,
  name: 'Alex'
}

Вложенное приведение типов

Глубокие структуры

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

const Product = object({
  id: coerce(
    number(),
    string(),
    Number
  ),

  price: coerce(
    number(),
    string(),
    Number
  )
})

const Order = object({
  products: array(Product)
})

const result = create({
  products: [
    {
      id: '1',
      price: '100'
    },
    {
      id: '2',
      price: '250'
    }
  ]
}, Order)

console.log(result)

Использование встроенных функций

Короткая запись

const NumberStruct = coerce(
  number(),
  string(),
  Number
)

Преобразование через Boolean

const BoolStruct = coerce(
  boolean(),
  string(),
  Boolean
)

Однако такой вариант может работать неожиданно:

Boolean('false')

Результат:

true

Поскольку любая непустая строка в JavaScript является truthy-значением.


Комбинирование с defaulted

Автоматическое приведение типов часто используется вместе с установкой значений по умолчанию.

Пример

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

const Config = object({
  port: defaulted(
    coerce(
      number(),
      string(),
      Number
    ),
    3000
  ),

  host: defaulted(
    string(),
    'localhost'
  )
})

console.log(create({}, Config))

Результат:

{
  port: 3000,
  host: 'localhost'
}

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

Важно понимать порядок выполнения:

  1. Применяется defaulted
  2. Выполняется coerce
  3. Выполняется валидация

Обработка массивов

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

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

const NumberArray = array(
  coerce(
    number(),
    string(),
    Number
  )
)

const result = create(
  ['1', '2', '3'],
  NumberArray
)

console.log(result)

Результат:

[1, 2, 3]

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

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

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

const NullableNumber = nullable(
  coerce(
    number(),
    string(),
    Number
  )
)

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

Преобразование union-типов

Работа с несколькими форматами

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

const NumericValue = union([
  number(),

  coerce(
    number(),
    string(),
    Number
  )
])

console.log(create('100', NumericValue))
console.log(create(200, NumericValue))

Очистка данных перед преобразованием

Перед приведением типов часто требуется нормализация данных.

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

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

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

Замена разделителей

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

console.log(
  create('12,55', PriceStruct)
)

Защита от NaN

Проблема

Number('abc')

Результат:

NaN

Тип при этом остаётся number.


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

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

const SafeNumber = define(
  'SafeNumber',
  value => {
    return (
      typeof value === 'number' &&
      !Number.isNaN(value)
    )
  }
)

const NumberStruct = coerce(
  SafeNumber,
  string(),
  Number
)

create('42', NumberStruct)

Массовая обработка API-данных

Типичный пример REST API

const ApiUser = {
  id: '1',
  age: '30',
  active: 'true',
  createdAt: '2025-01-10T12:00:00Z'
}

Нормализация структуры

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

const UserStruct = object({
  id: coerce(
    number(),
    string(),
    Number
  ),

  age: coerce(
    number(),
    string(),
    Number
  ),

  active: coerce(
    boolean(),
    string(),
    value => value === 'true'
  ),

  createdAt: coerce(
    date(),
    string(),
    value => new Date(value)
  )
})

const user = create(
  ApiUser,
  UserStruct
)

Повторное использование coercion-структур

Создание универсальных типов

export const NumberFromString = coerce(
  number(),
  string(),
  Number
)

export const BooleanFromString = coerce(
  boolean(),
  string(),
  value => value === 'true'
)

Композиция структур

import { object } from 'superstruct'

const BaseEntity = object({
  id: NumberFromString
})

const User = object({
  id: NumberFromString,
  age: NumberFromString
})

Производительность

Автоматическое преобразование добавляет дополнительный этап обработки:

  1. Проверка входного типа
  2. Вызов функции преобразования
  3. Повторная валидация результата

При обработке больших массивов данных это может влиять на производительность.


Оптимизация

Избегание тяжёлых операций

Плохо:

coerce(number(), string(), value => {
  return expensiveOperation(value)
})

Лучше:

coerce(number(), string(), Number)

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

Функция преобразования должна быть:

  • детерминированной;
  • без побочных эффектов;
  • синхронной;
  • максимально простой.

Антипаттерны

Асинхронные операции

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

coerce(number(), string(), async value => {
  return Number(value)
})

coerce не поддерживает Promise.


Изменение внешнего состояния

Плохо:

let counter = 0

const Struct = coerce(
  number(),
  string(),
  value => {
    counter++
    return Number(value)
  }
)

Преобразование должно быть чистой функцией.


Использование вместе с TypeScript

Типизированные структуры

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

const UserStruct = object({
  id: coerce(
    number(),
    string(),
    Number
  ),

  name: string()
})

type User = Infer<typeof UserStruct>

После преобразования:

User['id']

имеет тип:

number

Практический шаблон для production

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

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

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

    return result
  }
)

const BooleanField = coerce(
  boolean(),
  string(),
  value => {
    return value === 'true'
  }
)

const DateField = coerce(
  date(),
  string(),
  value => {
    const result = new Date(value)

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

    return result
  }
)

export const UserStruct = object({
  id: NumberField,
  active: BooleanField,
  createdAt: DateField
})