Базовые схемы: String, Number, Boolean и их рефайнменты
Строковые схемы и рефайнменты (длина, паттерн, trim, lowercase). Числовые схемы (int, positive, between, finite). Boolean и трансформации. Литеральные типы и перечисления. Nullable/Optional. Date, BigDecimal. Кастомные рефайнменты через filter. Коллекции: Array, Tuple, Record. Union. Аннотации.
Введение: атомы доменного моделирования
Любая сложная доменная модель строится из примитивных строительных блоков. В Effect Schema эти блоки — базовые схемы для примитивных типов TypeScript. Но в отличие от голых string, number и boolean, Schema позволяет уточнять (refine) эти типы, добавляя бизнес-ограничения прямо в определение.
Ключевой принцип: примитивные типы TypeScript слишком широки для домена. string может содержать пустую строку, строку длиной в миллион символов, невалидный email — что угодно. Наша задача — сузить типы до того, что действительно допустимо в нашем домене.
Schema.String — строковые типы
Базовая схема
import { Schema } from "effect"
// Базовая строковая схема — принимает любую строку
const Name = Schema.String
type Name = typeof Name.Type // string
// Decode — проверяет, что значение является строкой
Schema.decodeUnknownSync(Name)("hello") // "hello"
Schema.decodeUnknownSync(Name)(42) // throws ParseError
Schema.decodeUnknownSync(Name)(null) // throws ParseError
Рефайнменты для строк
Рефайнмент (refinement) — это сужение типа через добавление ограничения. Schema предоставляет богатый набор встроенных рефайнментов:
Ограничения длины
import { Schema } from "effect"
// Непустая строка — фундаментальный рефайнмент для домена
const NonEmpty = Schema.String.pipe(
Schema.nonEmptyString()
)
// Принимает: "hello", "a", " " (пробел — валидный символ)
// Отвергает: ""
// Минимальная длина
const AtLeast3 = Schema.String.pipe(
Schema.minLength(3)
)
// Принимает: "abc", "hello world"
// Отвергает: "", "ab"
// Максимальная длина
const AtMost100 = Schema.String.pipe(
Schema.maxLength(100)
)
// Точная длина
const Code6 = Schema.String.pipe(
Schema.length(6)
)
// Только строки длиной ровно 6 символов
// Комбинация — диапазон длины
const Username = Schema.String.pipe(
Schema.minLength(3),
Schema.maxLength(30),
Schema.annotations({
title: "Username",
description: "Имя пользователя от 3 до 30 символов"
})
)
Паттерны (регулярные выражения)
// Паттерн — проверка через регулярное выражение
const HexColor = Schema.String.pipe(
Schema.pattern(/^#[0-9a-fA-F]{6}$/),
Schema.annotations({ title: "HexColor", description: "Цвет в формате #RRGGBB" })
)
// Slug для URL
const Slug = Schema.String.pipe(
Schema.pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
Schema.minLength(1),
Schema.maxLength(100)
)
// UUID v4
const UUIDv4 = Schema.String.pipe(
Schema.pattern(
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
)
)
Встроенные строковые фильтры
Effect Schema предоставляет готовые рефайнменты для распространённых случаев:
// Trimmed — строка без ведущих/завершающих пробелов
const TrimmedString = Schema.String.pipe(Schema.trimmed())
// Принимает: "hello", "hello world"
// Отвергает: " hello", "hello ", " hello "
// Lowercased
const LowercaseString = Schema.String.pipe(Schema.lowercased())
// Uppercased
const UppercaseString = Schema.String.pipe(Schema.uppercased())
// Includes — содержит подстроку
const ContainsAt = Schema.String.pipe(Schema.includes("@"))
// StartsWith / EndsWith
const HttpUrl = Schema.String.pipe(Schema.startsWith("https://"))
const DotCom = Schema.String.pipe(Schema.endsWith(".com"))
// UUID (встроенный)
const Id = Schema.UUID
// Эквивалент: Schema.String.pipe(Schema.pattern(/^[0-9a-fA-F]{8}-...$/))
// ULID (встроенный)
const TimeBasedId = Schema.ULID
Трансформации строк
Помимо проверок, Schema может трансформировать строки:
// Trim — автоматически убирает пробелы при decode
const TrimmedInput = Schema.Trim
// Type = string, Encoded = string
// decode(" hello ") → "hello"
// encode("hello") → "hello"
// Lowercase
const NormalizedEmail = Schema.Lowercase
// decode("User@Example.COM") → "user@example.com"
// Compose — trim + lowercase
const CleanInput = Schema.Trim.pipe(Schema.compose(Schema.Lowercase))
// decode(" Hello World ") → "hello world"
// Split — строка → массив
const CommaSeparated = Schema.split(",")
// Type = ReadonlyArray<string>, Encoded = string
// decode("a,b,c") → ["a", "b", "c"]
Доменные строковые типы: примеры
Вот как строковые рефайнменты используются для доменного моделирования Todo-приложения:
import { Schema } from "effect"
// ═══════════════════════════════════════
// Доменные строковые типы для Todo
// ═══════════════════════════════════════
/** Заголовок задачи: непустая строка до 255 символов, автоматический trim */
const TodoTitle = Schema.Trim.pipe(
Schema.nonEmptyString(),
Schema.maxLength(255),
Schema.annotations({
title: "TodoTitle",
description: "Заголовок задачи — от 1 до 255 символов"
})
)
/** Описание задачи: опциональное, до 5000 символов */
const TodoDescription = Schema.String.pipe(
Schema.maxLength(5000),
Schema.annotations({
title: "TodoDescription",
description: "Подробное описание задачи"
})
)
/** Тег задачи: lowercase slug */
const TodoTag = Schema.Trim.pipe(
Schema.compose(Schema.Lowercase),
Schema.nonEmptyString(),
Schema.maxLength(50),
Schema.pattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/),
Schema.annotations({
title: "TodoTag",
description: "Тег задачи в формате slug (например: work, high-priority)"
})
)
Schema.Number — числовые типы
Базовая схема
const Age = Schema.Number
type Age = typeof Age.Type // number
Schema.decodeUnknownSync(Age)(42) // 42
Schema.decodeUnknownSync(Age)("42") // throws — строка не число!
Schema.decodeUnknownSync(Age)(NaN) // NaN — ОСТОРОЖНО!
Важно:
Schema.NumberпринимаетNaNиInfinity. Если это нежелательно (а в домене это почти всегда нежелательно), используйтеSchema.Finiteили добавьте рефайнмент.
Рефайнменты для чисел
import { Schema } from "effect"
// Целое число
const Int = Schema.Number.pipe(Schema.int())
// Принимает: 1, 42, -7, 0
// Отвергает: 3.14, 2.5
// Положительное число
const Positive = Schema.Number.pipe(Schema.positive())
// Принимает: 1, 0.5, 100
// Отвергает: 0, -1
// Неотрицательное число
const NonNegative = Schema.Number.pipe(Schema.nonNegative())
// Принимает: 0, 1, 100
// Отвергает: -1, -0.5
// Отрицательное число
const Negative = Schema.Number.pipe(Schema.negative())
// Диапазон (inclusive)
const Percentage = Schema.Number.pipe(
Schema.greaterThanOrEqualTo(0),
Schema.lessThanOrEqualTo(100)
)
// Диапазон (exclusive)
const StrictlyBetween = Schema.Number.pipe(
Schema.greaterThan(0),
Schema.lessThan(100)
)
// Конечное число (не NaN, не Infinity)
const FiniteNumber = Schema.Number.pipe(Schema.finite())
// или просто:
const SafeNumber = Schema.Finite
// Between — удобный shortcut
const Rating = Schema.Number.pipe(
Schema.between(1, 5)
)
// MultipleOf — кратность
const EvenNumber = Schema.Number.pipe(Schema.multipleOf(2))
Числовые трансформации
// NumberFromString — парсинг числа из строки
const Port = Schema.NumberFromString.pipe(
Schema.int(),
Schema.between(1, 65535)
)
// Type = number, Encoded = string
// decode("8080") → 8080
// decode("abc") → ParseError
// Clamp — ограничение диапазона (не отвергает, а обрезает)
const ClampedRating = Schema.Number.pipe(
Schema.clamp(1, 5)
)
// decode(0) → 1 (clamped to min)
// decode(3) → 3 (unchanged)
// decode(10) → 5 (clamped to max)
Доменные числовые типы: примеры
import { Schema } from "effect"
/** Приоритет задачи (1-5, целое число) */
const PriorityLevel = Schema.Number.pipe(
Schema.int(),
Schema.between(1, 5),
Schema.annotations({
title: "PriorityLevel",
description: "Числовой приоритет задачи от 1 (низкий) до 5 (критический)"
})
)
/** Процент выполнения */
const CompletionPercent = Schema.Number.pipe(
Schema.finite(),
Schema.greaterThanOrEqualTo(0),
Schema.lessThanOrEqualTo(100),
Schema.annotations({
title: "CompletionPercent",
description: "Процент выполнения задачи (0-100)"
})
)
/** Позиция в списке (целое неотрицательное) */
const SortOrder = Schema.Number.pipe(
Schema.int(),
Schema.nonNegative(),
Schema.annotations({ title: "SortOrder" })
)
/** Максимальное количество задач в списке */
const MaxTodos = Schema.Number.pipe(
Schema.int(),
Schema.between(1, 1000),
Schema.annotations({ title: "MaxTodos" })
)
Schema.Boolean — логический тип
Базовая схема
const IsActive = Schema.Boolean
type IsActive = typeof IsActive.Type // boolean
Schema.decodeUnknownSync(IsActive)(true) // true
Schema.decodeUnknownSync(IsActive)(false) // false
Schema.decodeUnknownSync(IsActive)(1) // throws — не boolean!
Schema.decodeUnknownSync(IsActive)("true") // throws — не boolean!
Важно:
Schema.Booleanстрогий — принимает толькоtrueиfalse. Числа0/1и строки"true"/"false"— не boolean.
Трансформации для Boolean
Часто внешние системы представляют boolean не как true/false:
import { Schema } from "effect"
// Из числа: 0 → false, всё остальное → true
const BoolFromNumber = Schema.transform(
Schema.Number,
Schema.Boolean,
{
strict: true,
decode: (n) => n !== 0,
encode: (b) => (b ? 1 : 0)
}
)
// decode(0) → false
// decode(1) → true
// encode(true) → 1
// encode(false) → 0
// Из строки "true"/"false"
const BoolFromString = Schema.transform(
Schema.Literal("true", "false"),
Schema.Boolean,
{
strict: true,
decode: (s) => s === "true",
encode: (b) => (b ? "true" as const : "false" as const)
}
)
// SQLite-специфичный: 0/1 как INTEGER
const SqliteBool = Schema.transform(
Schema.Literal(0, 1),
Schema.Boolean,
{
strict: true,
decode: (n) => n === 1,
encode: (b) => (b ? 1 as const : 0 as const)
}
)
Boolean в домене
/** Флаг завершения задачи */
const IsCompleted = Schema.Boolean.pipe(
Schema.annotations({
title: "IsCompleted",
description: "Флаг завершения задачи"
})
)
/** Флаг архивации */
const IsArchived = Schema.Boolean.pipe(
Schema.annotations({
title: "IsArchived",
description: "Помечена ли задача как архивная"
})
)
Литеральные типы и перечисления
Schema.Literal
Schema.Literal определяет схему, принимающую только указанные значения:
// Одно значение
const Admin = Schema.Literal("admin")
type Admin = typeof Admin.Type // "admin"
// Несколько значений — union
const Role = Schema.Literal("admin", "user", "guest")
type Role = typeof Role.Type // "admin" | "user" | "guest"
Schema.decodeUnknownSync(Role)("admin") // "admin"
Schema.decodeUnknownSync(Role)("superadmin") // throws ParseError
// Числовые литералы
const HttpStatus = Schema.Literal(200, 201, 204, 400, 404, 500)
type HttpStatus = typeof HttpStatus.Type // 200 | 201 | 204 | 400 | 404 | 500
// Смешанные литералы
const SpecialValue = Schema.Literal("none", 0, false, null)
type SpecialValue = typeof SpecialValue.Type // "none" | 0 | false | null
Enums в домене через Literal
Для доменного моделирования Schema.Literal — идеальный способ определить перечисления:
import { Schema } from "effect"
// ═══════════════════════════════════════
// Статус задачи — ключевое перечисление домена
// ═══════════════════════════════════════
const TodoStatus = Schema.Literal("draft", "active", "completed", "archived")
type TodoStatus = typeof TodoStatus.Type
// "draft" | "active" | "completed" | "archived"
// ═══════════════════════════════════════
// Приоритет задачи
// ═══════════════════════════════════════
const Priority = Schema.Literal("low", "medium", "high", "critical")
type Priority = typeof Priority.Type
// "low" | "medium" | "high" | "critical"
// ═══════════════════════════════════════
// Тип уведомления
// ═══════════════════════════════════════
const NotificationType = Schema.Literal("email", "push", "sms", "in-app")
type NotificationType = typeof NotificationType.Type
Schema.Enums — для TypeScript enum
Если в проекте используются TypeScript enums (хотя в функциональном стиле предпочтительнее Literal):
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT"
}
const DirectionSchema = Schema.Enums(Direction)
type DirectionType = typeof DirectionSchema.Type // Direction
Schema.decodeUnknownSync(DirectionSchema)("UP") // Direction.Up
Schema.decodeUnknownSync(DirectionSchema)("DIAGONAL") // throws
Рекомендация: в функциональном стиле с Effect предпочитайте
Schema.Literalвместо TypeScriptenum. Literal даёт union type, который лучше интегрируется с pattern matching и функциональной композицией.
Специальные типы
Nullable и Optional
import { Schema } from "effect"
// Nullable — string | null
const NullableString = Schema.NullOr(Schema.String)
type NullableString = typeof NullableString.Type // string | null
Schema.decodeUnknownSync(NullableString)("hello") // "hello"
Schema.decodeUnknownSync(NullableString)(null) // null
Schema.decodeUnknownSync(NullableString)(undefined) // throws!
// Undefined — string | undefined
const OptionalString = Schema.UndefinedOr(Schema.String)
// NullishOr — string | null | undefined
const NullishString = Schema.NullishOr(Schema.String)
Date
// Schema.Date — принимает Date объект
const EventDate = Schema.Date
Schema.decodeUnknownSync(EventDate)(new Date()) // Date
Schema.decodeUnknownSync(EventDate)("2024-01-01") // throws — не Date!
// Schema.DateFromString — трансформация string ↔ Date
const CreatedAt = Schema.DateFromString
// Type = Date, Encoded = string
Schema.decodeUnknownSync(CreatedAt)("2024-01-01T00:00:00Z") // Date
// Schema.encodeSync(CreatedAt)(new Date()) → "2024-01-01T00:00:00.000Z"
// DateFromNumber — timestamp ↔ Date
const Timestamp = Schema.DateFromNumber
// Type = Date, Encoded = number
Schema.decodeUnknownSync(Timestamp)(1704067200000) // Date
BigDecimal и BigInt
Для финансовых расчётов, где floating-point неточности недопустимы:
import { Schema } from "effect"
// BigDecimal из числа
const Price = Schema.BigDecimalFromNumber
// Type = BigDecimal, Encoded = number
// BigDecimal из строки
const ExactAmount = Schema.BigDecimal
// Type = BigDecimal, Encoded = string
// BigInt
const LargeId = Schema.BigInt
// Type = bigint, Encoded = string
Кастомные рефайнменты через Schema.filter
Когда встроенных рефайнментов недостаточно, используем Schema.filter:
import { Schema } from "effect"
// Простой фильтр с boolean
const EvenNumber = Schema.Number.pipe(
Schema.filter((n) => n % 2 === 0, {
message: () => "Число должно быть чётным"
})
)
// Фильтр с кастомным сообщением об ошибке
const StrongPassword = Schema.String.pipe(
Schema.minLength(8),
Schema.filter(
(s) => /[A-Z]/.test(s) && /[a-z]/.test(s) && /[0-9]/.test(s),
{
message: () =>
"Пароль должен содержать заглавную букву, строчную букву и цифру"
}
)
)
// Фильтр с детальной ошибкой (ParseResult.Type)
const ValidDateRange = Schema.Struct({
start: Schema.DateFromString,
end: Schema.DateFromString
}).pipe(
Schema.filter(
({ start, end }) => end > start,
{
message: () => "Дата окончания должна быть позже даты начала"
}
)
)
// Фильтр, возвращающий массив ошибок
const ComplexValidation = Schema.String.pipe(
Schema.filter((s) => {
const issues: Array<Schema.FilterIssue> = []
if (s.length < 8) {
issues.push({ path: [], message: "Минимальная длина — 8 символов" })
}
if (!/[A-Z]/.test(s)) {
issues.push({ path: [], message: "Нужна хотя бы одна заглавная буква" })
}
if (!/[0-9]/.test(s)) {
issues.push({ path: [], message: "Нужна хотя бы одна цифра" })
}
return issues.length === 0 ? true : issues
})
)
Типизированный фильтр с brand
Часто фильтр сочетают с brand, чтобы создать номинальный тип:
import { Schema } from "effect"
// Email — строка + валидация + brand
const Email = Schema.String.pipe(
Schema.trimmed(),
Schema.lowercased(),
Schema.nonEmptyString(),
Schema.maxLength(254),
Schema.pattern(/^[^@\s]+@[^@\s]+\.[^@\s]+$/),
Schema.brand("Email"),
Schema.annotations({
title: "Email",
description: "Валидный email-адрес"
})
)
type Email = typeof Email.Type
// string & Brand<"Email">
// Теперь Email — это отдельный тип
const sendNotification = (email: Email): void => {
// email гарантированно валиден
console.log(`Sending to ${email}`)
}
// ❌ Ошибка компиляции
sendNotification("not-validated@email.com")
// Type 'string' is not assignable to type 'string & Brand<"Email">'
// ✅ Только через Schema.decode
const validEmail = Schema.decodeUnknownSync(Email)("user@example.com")
sendNotification(validEmail) // OK
Аннотации: метаданные для Schema
Каждая Schema может быть аннотирована метаданными, которые полезны для документации, генерации OpenAPI, отображения ошибок:
import { Schema } from "effect"
const TodoTitle = Schema.String.pipe(
Schema.nonEmptyString(),
Schema.maxLength(255),
Schema.annotations({
// Стандартные аннотации
title: "TodoTitle",
description: "Заголовок задачи — непустая строка до 255 символов",
examples: ["Купить молоко", "Подготовить отчёт за Q3"],
// Кастомные аннотации (JSON Schema)
jsonSchema: {
minLength: 1,
maxLength: 255
},
// Сообщение об ошибке
message: () => "Заголовок задачи должен быть от 1 до 255 символов"
})
)
Аннотации не влияют на валидацию, но используются:
- В сообщениях об ошибках (
message) - При генерации JSON Schema (
jsonSchema) - При генерации OpenAPI спецификации
- При генерации документации
- В тестах (
examples)
Unknown, Any, Void, Never
// Unknown — принимает любое значение, тип unknown
const AnyValue = Schema.Unknown
type AnyValue = typeof AnyValue.Type // unknown
// Any — аналогично, но тип any (менее безопасно)
const Unsafe = Schema.Any
type Unsafe = typeof Unsafe.Type // any
// Void — для функций, не возвращающих значение
const NoReturn = Schema.Void
type NoReturn = typeof NoReturn.Type // void
// Never — схема, которая отвергает всё
const Impossible = Schema.Never
// Schema.decodeUnknownSync(Impossible)(anything) — всегда throws
// Undefined
const Undef = Schema.Undefined
Schema.decodeUnknownSync(Undef)(undefined) // undefined
Schema.decodeUnknownSync(Undef)(null) // throws
// Null
const Nul = Schema.Null
Schema.decodeUnknownSync(Nul)(null) // null
Schema.decodeUnknownSync(Nul)(undefined) // throws
Коллекции: Array, ReadonlyArray, Set, Map
import { Schema } from "effect"
// ═══════════════════════════════════════
// Массивы
// ═══════════════════════════════════════
// ReadonlyArray — иммутабельный массив (предпочтительно)
const Tags = Schema.Array(Schema.String)
type Tags = typeof Tags.Type // ReadonlyArray<string>
// С рефайнментами на элементы
const Priorities = Schema.Array(
Schema.Literal("low", "medium", "high")
)
type Priorities = typeof Priorities.Type
// ReadonlyArray<"low" | "medium" | "high">
// С рефайнментами на массив
const NonEmptyTags = Schema.NonEmptyArray(Schema.String)
type NonEmptyTags = typeof NonEmptyTags.Type
// readonly [string, ...Array<string>]
// Ограничения на длину массива
const LimitedTags = Schema.Array(Schema.String).pipe(
Schema.minItems(1),
Schema.maxItems(10)
)
// ═══════════════════════════════════════
// Кортежи (Tuples)
// ═══════════════════════════════════════
// Фиксированная структура
const Point2D = Schema.Tuple(Schema.Number, Schema.Number)
type Point2D = typeof Point2D.Type // readonly [number, number]
const NameAge = Schema.Tuple(Schema.String, Schema.Number)
type NameAge = typeof NameAge.Type // readonly [string, number]
// Кортеж с rest
const StringWithNumbers = Schema.Tuple(
[Schema.String], // первый элемент — string
Schema.Number // остальные — number
)
type StringWithNumbers = typeof StringWithNumbers.Type
// readonly [string, ...Array<number>]
// ═══════════════════════════════════════
// Record (словарь)
// ═══════════════════════════════════════
const ScoresByPlayer = Schema.Record({
key: Schema.String,
value: Schema.Number
})
type ScoresByPlayer = typeof ScoresByPlayer.Type
// { readonly [x: string]: number }
// С ограниченным ключом
const StatusCounts = Schema.Record({
key: Schema.Literal("active", "completed", "archived"),
value: Schema.Number
})
// ═══════════════════════════════════════
// Set и Map (через трансформации)
// ═══════════════════════════════════════
// HashSet из массива
const UniqueTagsFromArray = Schema.HashSetFromSelf(Schema.String)
// HashMap
const SettingsMap = Schema.HashMapFromSelf({
key: Schema.String,
value: Schema.Unknown
})
Объединения и пересечения
Union — “или”
import { Schema } from "effect"
// Простой union
const StringOrNumber = Schema.Union(Schema.String, Schema.Number)
type StringOrNumber = typeof StringOrNumber.Type // string | number
// Дискриминированный union (tagged union)
const Shape = Schema.Union(
Schema.Struct({
_tag: Schema.Literal("Circle"),
radius: Schema.Number.pipe(Schema.positive())
}),
Schema.Struct({
_tag: Schema.Literal("Rectangle"),
width: Schema.Number.pipe(Schema.positive()),
height: Schema.Number.pipe(Schema.positive())
}),
Schema.Struct({
_tag: Schema.Literal("Triangle"),
base: Schema.Number.pipe(Schema.positive()),
height: Schema.Number.pipe(Schema.positive())
})
)
type Shape = typeof Shape.Type
// { _tag: "Circle"; radius: number }
// | { _tag: "Rectangle"; width: number; height: number }
// | { _tag: "Triangle"; base: number; height: number }
// Discriminated union с дискриминантом
// Schema автоматически определяет _tag как дискриминант
const decoded = Schema.decodeUnknownSync(Shape)({
_tag: "Circle",
radius: 5
})
// { _tag: "Circle", radius: 5 }
Optional fields в Struct
import { Schema } from "effect"
const Todo = Schema.Struct({
id: Schema.String,
title: Schema.String,
// optional — поле может отсутствовать (undefined)
description: Schema.optional(Schema.String),
// optionalWith с exact: true — строго undefined, не missing
notes: Schema.optional(Schema.String, { exact: true }),
// optionalWith default — значение по умолчанию при decode
completed: Schema.optional(Schema.Boolean, { default: () => false }),
// optionalWith nullable
dueDate: Schema.optional(Schema.DateFromString, { nullable: true }),
})
type Todo = typeof Todo.Type
// {
// readonly id: string
// readonly title: string
// readonly description?: string | undefined
// readonly notes?: string | undefined
// readonly completed: boolean ← default превращает optional в required!
// readonly dueDate?: Date | null | undefined
// }
Паттерны композиции базовых схем
Pipe — последовательное уточнение
import { Schema } from "effect"
// pipe — основной способ композиции рефайнментов
const SafePositiveInt = Schema.Number.pipe(
Schema.finite(), // шаг 1: исключить NaN, Infinity
Schema.int(), // шаг 2: только целые
Schema.positive(), // шаг 3: только положительные
Schema.lessThan(2147483647) // шаг 4: вписывается в int32
)
// Каждый рефайнмент добавляет проверку, сужая допустимые значения
Вынесение общих паттернов
import { Schema } from "effect"
// ═══════════════════════════════════════
// Переиспользуемые базовые схемы
// ═══════════════════════════════════════
/** Непустая строка с автоматическим trim */
const CleanString = Schema.Trim.pipe(
Schema.nonEmptyString()
)
/** Ограниченная строка */
const boundedString = (min: number, max: number) =>
CleanString.pipe(
Schema.minLength(min),
Schema.maxLength(max)
)
/** Положительное целое число */
const PositiveInt = Schema.Number.pipe(
Schema.int(),
Schema.positive()
)
/** Неотрицательное целое */
const NonNegativeInt = Schema.Number.pipe(
Schema.int(),
Schema.nonNegative()
)
/** ISO 8601 дата-время */
const ISODateTime = Schema.DateFromString
// ═══════════════════════════════════════
// Использование в доменных типах
// ═══════════════════════════════════════
const TodoTitle = boundedString(1, 255).pipe(
Schema.brand("TodoTitle"),
Schema.annotations({ title: "TodoTitle" })
)
const TodoDescription = boundedString(1, 5000).pipe(
Schema.annotations({ title: "TodoDescription" })
)
const MaxItems = PositiveInt.pipe(
Schema.lessThanOrEqualTo(1000),
Schema.annotations({ title: "MaxItems" })
)
Итоги главы
Базовые схемы — это атомы доменного моделирования. Ключевые принципы:
- Сужайте примитивы: не используйте голый
string— уточните длину, паттерн, формат - Композируйте через pipe: каждый рефайнмент — это шаг сужения от широкого типа к узкому
- Используйте brand для номинальной типизации:
Email≠string, даже если runtime-значение — строка - Выносите общие паттерны:
boundedString(min, max),PositiveInt— переиспользуемые строительные блоки - Аннотируйте:
title,description,examples— для документации и сообщений об ошибках - Помните о двойственности Type/Encoded:
DateFromStringдекодируетstringвDateи обратно
В следующей главе мы перейдём от отдельных примитивов к составным структурам — Schema.Struct и Schema.Class, которые позволяют моделировать полноценные сущности домена.