Типизация схем

Типизация в Superstruct строится вокруг идеи строгого описания данных через композицию примитивов и структур. В отличие от рантайм-валидации без контекста типов, здесь каждая схема может быть связана с TypeScript-типом, что позволяет синхронизировать проверку данных и систему типов.

Ключевая особенность подхода — двойственная природа структур: они одновременно являются и валидаторами, и источником типов.


Базовое соответствие структур и типов

Каждый примитив Superstruct соответствует конкретному TypeScript-типу:

  • string()string
  • number()number
  • boolean()boolean
  • literal(value) → точечный литерал
  • array(struct) → массив соответствующего типа
  • object({...}) → объект с типизированными полями

Пример базовой структуры:

import { struct } from "superstruct"

const User = struct({
  id: "number",
  name: "string",
  active: "boolean"
})

TypeScript-интерпретация такой схемы:

type User = {
  id: number
  name: string
  active: boolean
}

Таким образом, структура фактически описывает контракт данных.


Выведение типов через Infer

Одним из ключевых механизмов является автоматическое извлечение типа структуры.

import { Infer } from "superstruct"

type User = Infer<typeof User>

Это позволяет поддерживать единый источник истины: схема становится одновременно валидатором и типовым описанием.

Особенно важно, что при изменении структуры тип обновляется автоматически, исключая рассинхронизацию между рантаймом и компиляцией.


Литералы и узкая типизация

Литеральные значения позволяют создавать строго ограниченные типы.

const Role = struct.union([
  struct.literal("admin"),
  struct.literal("user"),
  struct.literal("guest")
])

TypeScript-эквивалент:

type Role = "admin" | "user" | "guest"

Это даёт возможность строить безопасные дискретные множества значений без ручного перечисления типов.


Объединения типов (union)

Union-типы позволяют описывать данные, которые могут соответствовать нескольким схемам одновременно.

const Id = struct.union([
  "string",
  "number"
])

Это соответствует:

type Id = string | number

При валидации Superstruct проверяет каждую альтернативу последовательно до первого успешного совпадения.


Пересечения типов (intersection)

Пересечения позволяют объединять несколько структур в одну комплексную модель.

const Timestamped = struct({
  createdAt: "number"
})

const Named = struct({
  name: "string"
})

const Entity = struct.intersection([
  Timestamped,
  Named
])

Результат:

type Entity = {
  createdAt: number
  name: string
}

Пересечения особенно полезны при композиции доменных моделей.


Опциональные поля

Опциональность реализуется через обёртку optional.

const Profile = struct({
  bio: struct.optional("string"),
  avatar: struct.optional("string")
})

TypeScript:

type Profile = {
  bio?: string
  avatar?: string
}

Важно, что optional влияет не только на тип, но и на поведение валидации — отсутствие поля не считается ошибкой.


Значения по умолчанию

Схемы могут задавать дефолтные значения, расширяя типизацию.

const Settings = struct({
  theme: struct.defaulted("string", "light")
})

Тип:

type Settings = {
  theme: string
}

Хотя тип остаётся string, семантика схемы уточняет, что значение всегда будет присутствовать после обработки.


Вложенные структуры

Типизация глубоко распространяется на вложенные объекты.

const Post = struct({
  title: "string",
  author: struct({
    id: "number",
    name: "string"
  })
})

Результат:

type Post = {
  title: string
  author: {
    id: number
    name: string
  }
}

Вложенность не ограничена уровнем глубины, что позволяет описывать сложные доменные модели без потери строгой типизации.


Массивы и коллекции

Массивы типизируются через вложенную структуру элемента.

const Tags = struct.array("string")
type Tags = string[]

Для сложных объектов:

const Comments = struct.array(
  struct({
    id: "number",
    text: "string"
  })
)
type Comments = {
  id: number
  text: string
}[]

Частичные типы (partial)

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

const PatchUser = struct.partial({
  name: "string",
  age: "number"
})
type PatchUser = {
  name?: string
  age?: number
}

Это особенно важно для PATCH-запросов и частичных обновлений состояния.


Расширение типов через композицию

Типизация в Superstruct опирается на композиционный подход. Схемы можно расширять без дублирования.

const BaseUser = struct({
  id: "number"
})

const ExtendedUser = struct({
  ...BaseUser.schema,
  email: "string"
})

Это приводит к типу:

type ExtendedUser = {
  id: number
  email: string
}

Композиция позволяет строить иерархии моделей без потери статической строгости.


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

Superstruct поддерживает создание кастомных валидаторов, которые также могут быть типизированы.

import { struct, define } from "superstruct"

const PositiveNumber = define("PositiveNumber", (value) => {
  return typeof value === "number" && value > 0
})

TypeScript:

type PositiveNumber = number

Хотя тип остаётся базовым, семантика уточняется на уровне валидации.


Приведение типов (coercion)

Некоторые структуры поддерживают приведение типов, влияющее на итоговую форму данных.

const NumberFromString = struct.coerce("number", "string", (value) =>
  Number(value)
)

Это позволяет согласовать внешние данные (например, JSON API) с внутренней типизацией.


Дискриминированные объединения

Для сложных моделей часто используется паттерн discriminated unions.

const Success = struct({
  status: struct.literal("success"),
  data: "string"
})

const Failure = struct({
  status: struct.literal("error"),
  message: "string"
})

const Response = struct.union([Success, Failure])

TypeScript:

type Response =
  | { status: "success"; dat a: string }
  | { status: "error"; message: string }

Ключевое поле status служит дискриминатором.


Строгая иерархия типов

Superstruct позволяет строить строгие доменные модели, где каждый уровень данных описан схемой. Это создаёт систему, в которой:

  • структура данных формализована
  • типы синхронизированы с рантаймом
  • изменения схемы автоматически отражаются в коде

Такой подход минимизирует расхождения между логикой приложения и фактическими данными.


Ограничения типизации

Несмотря на мощную систему, типизация Superstruct имеет ограничения:

  • типы не всегда могут точно отражать пользовательские кастомные проверки
  • сложные зависимости между полями требуют ручной логики
  • некоторые runtime-валидации не транслируются в TypeScript напрямую

Тем не менее, базовый уровень строгости остаётся высоким и покрывает большинство прикладных сценариев.


Интеграция с TypeScript-проектами

В типичных приложениях Superstruct используется как слой между внешними данными и внутренними моделями. Типизация играет роль гаранта согласованности:

  • API-ответы приводятся к схемам
  • формы валидируются до отправки
  • бизнес-логика работает только с типизированными данными

Это позволяет выстраивать предсказуемую архитектуру данных без дублирования описаний типов.