Наименования в TypeScript / Effect-ts
Полный свод правил именования в TypeScript / Effect-ts.
1. Философия именования
1.1 Фундаментальные принципы
Именование в функциональном программировании — это контракт между автором и читателем кода. Каждое имя должно отвечать на три вопроса:
- Что это? (сущность, действие, свойство)
- Какой эффект? (чистая функция, эффектная, с побочными эффектами)
- Какова область действия? (локальная, модульная, глобальная)
1.2 Ключевые аксиомы
Аксиома 1 — Имя = Намерение. Имя должно полностью раскрывать намерение без необходимости читать реализацию.
Аксиома 2 — Глагол = Вычисление. В FP функции — это вычисления. Имя функции — это описание трансформации данных.
Аксиома 3 — Существительное = Данные. Переменные, типы, сущности — неизменяемые структуры данных. Их имена — существительные.
Аксиома 4 — Суффикс = Контекст. Суффиксы Live, Test, InMemory, Error, Schema — обязательные маркеры роли конструкции в системе.
Аксиома 5 — Префикс = Принадлежность. Префиксы модуля или домена (User, Order, Payment) группируют связанные конструкции.
1.3 Длина имени
| Область | Длина | Пример |
|---|---|---|
| Лямбда-параметр | 1–3 символа | x, a, el, _ |
| Параметр pipe/flow | 1–5 символов | user, req, cfg |
| Локальная переменная | 3–15 символов | userId, validatedEmail |
| Функция модуля | 5–30 символов | findUserByEmail |
| Тип / интерфейс | 5–40 символов | UserRepositoryLive |
| Файл | 5–30 символов | UserRepository.ts |
2. Базовые правила форматирования
2.1 Таблица стилей
| Конструкция | Стиль | Пример |
|---|---|---|
| Переменная | camelCase | userEmail |
| Функция | camelCase | createUser |
| Чистая константа | camelCase | maxRetries |
| Enum-like объект | UPPER_SNAKE | MAX_RETRY_COUNT |
| Тип / Interface | PascalCase | UserProfile |
| Class (Effect Tag/Error) | PascalCase | UserNotFoundError |
| Schema | PascalCase | CreateUserRequest |
| Generic параметр | PascalCase 1 буква или слово | A, E, R, Self |
| Файл (модуль) | PascalCase | UserRepository.ts |
| Файл (утилита/хелпер) | camelCase | hashPassword.ts |
| Директория | kebab-case | user-management/ |
| Env-переменная | UPPER_SNAKE | DATABASE_URL |
| Effect Tag identifier | PascalCase строка | "UserRepository" |
| Brand | PascalCase | "UserId", "Email" |
2.2 Строгие запреты
// ❌ ЗАПРЕЩЕНО: венгерская нотация
const sUserName = "John" // ❌
const iAge = 25 // ❌
const bIsActive = true // ❌
// ✅ ПРАВИЛЬНО: семантическое именование
const userName = "John" // ✅
const age = 25 // ✅
const isActive = true // ✅
// ❌ ЗАПРЕЩЕНО: аббревиатуры (кроме общепринятых)
const usrMgr = ... // ❌
const authCtrlr = ... // ❌
const db = ... // ⚠️ допустимо только в узком scope
// ✅ ПРАВИЛЬНО: полные слова
const userManager = ... // ✅
const authController = ... // ✅
const database = ... // ✅
// ❌ ЗАПРЕЩЕНО: числа в именах для различения версий
const processUser2 = ... // ❌
const handleRequestV2 = ... // ❌
// ✅ ПРАВИЛЬНО: семантическое различие
const processUserWithRetry = ... // ✅
const handleAuthenticatedRequest = ... // ✅
2.3 Допустимые сокращения
| Сокращение | Полная форма | Где допустимо |
|---|---|---|
id | identifier | Везде |
db | database | Внутри функции / лямбда |
req | request | HTTP handler, pipe |
res | response | HTTP handler, pipe |
cfg / config | configuration | Параметр функции |
ctx | context | Middleware, pipe |
err | error | Catch-блоки, error handlers |
fn | function | Higher-order функции |
acc | accumulator | Fold/reduce |
el | element | Map/filter лямбды |
env | environment | Config, Layer |
ref | reference | Effect Ref |
idx | index | Итерация |
prev | previous | Reduce, state transitions |
curr | current | Reduce, state transitions |
3. Переменные и константы
3.1 Immutable-first: всё — const
// ✅ ПРАВИЛЬНО: только const
const userName = "Vladimir" as const
const userIds = [1, 2, 3] as const
const config = { port: 3000, host: "localhost" } as const
// ❌ ЗАПРЕЩЕНО: let (кроме исключительных случаев в imperative interop)
let counter = 0 // ❌ — используй Ref в Effect
3.2 Булевы переменные
Всегда начинаются с глагольного префикса:
| Префикс | Семантика | Пример |
|---|---|---|
is | Состояние / бытие | isActive, isValid, isEmpty |
has | Владение / наличие | hasPermission, hasChildren |
can | Возможность / способность | canEdit, canDelete |
should | Рекомендация / ожидание | shouldRetry, shouldNotify |
was | Прошедшее состояние | wasProcessed, wasDeleted |
will | Будущее действие | willExpire, willMigrate |
needs | Необходимость | needsAuth, needsValidation |
did | Завершённое действие | didComplete, didFail |
allows | Разрешение | allowsGuest, allowsEmpty |
// ✅ Полные примеры
const isEmailVerified = true
const hasAdminRole = pipe(user.roles, ReadonlyArray.contains("admin"))
const canAccessDashboard = isEmailVerified && hasAdminRole
const shouldSendNotification = !user.isOptedOut && hasNewMessages
const needsPasswordReset = pipe(
lastPasswordChange,
Duration.greaterThan(Duration.days(90))
)
3.3 Числовые переменные
// Счётчики и количества — суффикс Count
const retryCount = 3
const activeUserCount = 42
const failedAttemptCount = 0
// Максимумы/минимумы — префикс max/min
const maxRetries = 5
const minPasswordLength = 8
const maxConcurrentConnections = 100
// Размеры — суффикс Size или Length
const batchSize = 50
const bufferSize = 1024
const queueLength = 200
// Индексы — суффикс Index
const currentIndex = 0
const startIndex = 10
// Временные значения — описательные имена
const timeoutMs = 5000
const intervalSeconds = 30
const cacheTtlMinutes = 15
const sessionDurationHours = 24
3.4 Коллекции
// ✅ Множественное число для массивов и множеств
const users: ReadonlyArray<User> = []
const activeOrderIds: ReadonlySet<OrderId> = new Set()
const emailAddresses: ReadonlyArray<Email> = []
// ✅ Суффикс Map/Dict/Index для справочников
const userById: ReadonlyMap<UserId, User> = new Map()
const permissionsByRole: Record<Role, ReadonlyArray<Permission>> = {}
const priceIndex: ReadonlyMap<ProductId, Price> = new Map()
// ✅ Суффикс Queue/Stack/Buffer для структур
const taskQueue: ReadonlyArray<Task> = []
const undoStack: ReadonlyArray<Action> = []
const messageBuffer: ReadonlyArray<Message> = []
// ❌ ЗАПРЕЩЕНО
const userList = [] // ❌ — "List" избыточен
const userData = [] // ❌ — "Data" не информативен
const userArr = [] // ❌ — тип виден из типизации
const items = [] // ❌ — слишком абстрактно
3.5 Optional и Nullable
// ✅ Используем Option из Effect, имя не меняется
const maybeUser: Option.Option<User> = Option.none()
const foundUser: Option.Option<User> = Option.some(user)
// ✅ Если нужно подчеркнуть опциональность в примитивном контексте
const lastLoginAt: Option.Option<Date> = Option.none()
const parentId: Option.Option<UserId> = Option.none()
// ❌ ЗАПРЕЩЕНО: null-суффиксы, вопросительные знаки в именах
const userOrNull = null // ❌ — используй Option
const maybeNullUser = null // ❌
4. Функции
4.1 Чистые функции (Total Functions)
Чистые функции — глагол + существительное. Описывают трансформацию данных.
| Паттерн | Семантика | Пример |
|---|---|---|
verb + Noun | Основное действие | createUser, validateEmail |
get + Noun | Синхронное извлечение (чистое) | getFullName, getAge |
calc/compute + Noun | Вычисление | calcTotal, computeDiscount |
parse + Noun | Парсинг / десериализация | parseConfig, parseDate |
format + Noun | Форматирование | formatCurrency, formatDate |
to + Target | Конвертация типа | toJSON, toString, toDTO |
from + Source | Создание из источника | fromJSON, fromDTO, fromRow |
merge + Noun | Слияние | mergeConfigs, mergeHeaders |
split + Noun | Разделение | splitChunks, splitByDelimiter |
filter + Noun | Фильтрация | filterActive, filterByRole |
sort + By + Criterion | Сортировка | sortByCreatedAt, sortByName |
group + By + Key | Группировка | groupByStatus, groupByDate |
normalize + Noun | Нормализация | normalizeEmail, normalizePath |
sanitize + Noun | Очистка | sanitizeHtml, sanitizeInput |
extract + Noun | Извлечение части | extractDomain, extractToken |
build + Noun | Построение сложного объекта | buildQuery, buildResponse |
encode / decode | Кодирование / декодирование | encodeBase64, decodeJwt |
hash + Noun | Хеширование | hashPassword, hashContent |
// ✅ Чистые функции — существительное описывает результат
const getFullName = (user: User): string =>
`${user.firstName} ${user.lastName}`
const calcOrderTotal = (items: ReadonlyArray<OrderItem>): Money =>
pipe(
items,
ReadonlyArray.map((item) => item.price * item.quantity),
ReadonlyArray.reduce(Money.zero, Money.add)
)
const normalizeEmail = (raw: string): string =>
raw.trim().toLowerCase()
const filterActiveUsers = (users: ReadonlyArray<User>): ReadonlyArray<User> =>
pipe(users, ReadonlyArray.filter((u) => u.isActive))
const groupOrdersByStatus = (
orders: ReadonlyArray<Order>
): ReadonlyMap<OrderStatus, ReadonlyArray<Order>> =>
pipe(orders, ReadonlyArray.groupBy((o) => o.status))
// ✅ from/to — конвертация между типами
const fromDTO = (dto: UserDTO): User => ({
id: UserId(dto.id),
email: Email(dto.email),
createdAt: new Date(dto.created_at),
})
const toResponse = (user: User): UserResponse => ({
id: user.id,
displayName: getFullName(user),
memberSince: formatDate(user.createdAt),
})
4.2 Предикаты (Predicates)
Возвращают boolean. Именуются как булевы переменные.
// ✅ Предикаты — префикс is/has/can/should
const isAdult = (age: number): boolean => age >= 18
const isValidEmail = (email: string): boolean =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
const hasPermission = (user: User, permission: Permission): boolean =>
pipe(user.permissions, ReadonlyArray.contains(permission))
const canAccessResource = (
user: User,
resource: Resource
): boolean =>
isActive(user) && hasPermission(user, resource.requiredPermission)
// ✅ Предикаты для коллекций — без скобок в pipe
const isActive = (user: User): boolean => user.isActive
const isExpired = (session: Session): boolean =>
Date.now() > session.expiresAt
// Использование в pipe
pipe(
users,
ReadonlyArray.filter(isActive),
ReadonlyArray.filter(isAdult),
)
4.3 Функции-конструкторы (Smart Constructors)
// ✅ make — основной конструктор
const makeUser = (params: {
readonly name: string
readonly email: string
}): User => ({
id: UserId(generateId()),
name: params.name,
email: Email(params.email),
createdAt: new Date(),
isActive: true,
})
// ✅ makeDefault / empty — конструктор значений по умолчанию
const makeDefaultConfig = (): AppConfig => ({
port: 3000,
host: "localhost",
logLevel: "info",
})
const emptyCart = (): Cart => ({
items: [],
total: Money.zero,
})
// ✅ of — краткий конструктор (монадический стиль)
const ofUserId = (raw: string): UserId => UserId(raw)
// ✅ unsafeMake — конструктор без валидации (для тестов/interop)
const unsafeMakeEmail = (raw: string): Email => raw as Email
4.4 Effectful функции (с побочными эффектами)
Функции, возвращающие Effect, используют те же глаголы, но Effect в возвращаемом типе явно сигнализирует о наличии эффекта. Дополнительных маркеров в имени не требуется.
// ✅ Effectful функции — имя описывает ДЕЙСТВИЕ, тип описывает ЭФФЕКТ
const findUserById = (
id: UserId
): Effect.Effect<User, UserNotFoundError, UserRepository> =>
pipe(
UserRepository,
Effect.flatMap((repo) => repo.findById(id))
)
const sendWelcomeEmail = (
user: User
): Effect.Effect<void, EmailDeliveryError, EmailService> =>
pipe(
EmailService,
Effect.flatMap((service) => service.send({
to: user.email,
template: "welcome",
data: { name: user.name },
}))
)
const createUserAndNotify = (
input: CreateUserInput
): Effect.Effect<User, CreateUserError | EmailDeliveryError, UserRepository | EmailService> =>
pipe(
createUser(input),
Effect.tap((user) => sendWelcomeEmail(user))
)
Специфичные глаголы для Effect-операций:
| Глагол | Семантика | Пример |
|---|---|---|
find + By + Key | Поиск (может не найти) | findUserByEmail |
fetch + Noun | Получение извне (API, DB) | fetchOrderDetails |
save + Noun | Сохранение / персистенция | saveUser, saveOrder |
delete + Noun | Удаление | deleteSession, deleteUser |
update + Noun | Обновление | updateProfile, updateStock |
send + Noun | Отправка (email, event, msg) | sendEmail, sendNotification |
publish + Noun | Публикация события | publishOrderCreated |
subscribe + To | Подписка на события | subscribeToUpdates |
connect + To | Установка соединения | connectToDatabase |
disconnect | Разрыв соединения | disconnectFromBroker |
acquire + Noun | Захват ресурса (Scope) | acquireConnection |
release + Noun | Освобождение ресурса | releaseConnection |
retry + Noun | Повтор с политикой | retryRequest |
validate + Noun | Валидация (effectful) | validateToken |
authorize + Noun | Авторизация | authorizeRequest |
authenticate | Аутентификация | authenticateUser |
emit + Noun | Эмиссия события | emitMetric |
schedule + Noun | Планирование | scheduleCleanup |
spawn + Noun | Запуск Fiber | spawnWorker |
resolve + Noun | Разрешение / резолв | resolveDependency |
4.5 Higher-Order Functions
// ✅ Паттерн: withX — обёртка добавляющая поведение
const withRetry = <A, E, R>(
policy: Schedule.Schedule<unknown, E, never>
) => (self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
pipe(self, Effect.retry(policy))
const withTimeout = <A, E, R>(
duration: Duration.DurationInput
) => (self: Effect.Effect<A, E, R>): Effect.Effect<A, E | TimeoutError, R> =>
pipe(self, Effect.timeout(duration))
const withLogging = <A, E, R>(
label: string
) => (self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
pipe(
Effect.log(`Starting: ${label}`),
Effect.flatMap(() => self),
Effect.tap((result) => Effect.log(`Completed: ${label}`))
)
// ✅ Паттерн: makeX — фабрика функций
const makeValidator = <A>(
schema: Schema.Schema<A>
) => (input: unknown): Effect.Effect<A, ParseError> =>
Schema.decodeUnknown(schema)(input)
// ✅ Паттерн: onX — callback-обработчик
const onUserCreated = (handler: (user: User) => Effect.Effect<void>) =>
pipe(
EventBus,
Effect.flatMap((bus) => bus.subscribe("UserCreated", handler))
)
4.6 Функции-компараторы и эквиваленция
// ✅ Паттерн: xxxOrder / xxxEquivalence
const userByNameOrder: Order.Order<User> = pipe(
Order.string,
Order.mapInput((user: User) => user.name)
)
const userIdEquivalence: Equivalence.Equivalence<UserId> =
Equivalence.string
// ✅ Паттерн: compareByXxx
const compareByCreatedAt = Order.mapInput(
Order.Date,
(user: User) => user.createdAt
)
// Использование
pipe(users, ReadonlyArray.sort(userByNameOrder))
pipe(users, ReadonlyArray.sort(compareByCreatedAt))
5. Effect-специфичные конструкции
5.1 Effect-программы (Programs)
// ✅ Главная программа — всегда program или main
const program: Effect.Effect<void, AppError, AppDependencies> =
pipe(
initializeApp,
Effect.flatMap(() => startHttpServer),
Effect.flatMap(() => Effect.never) // keep alive
)
const main: Effect.Effect<void, never, never> =
pipe(
program,
Effect.provide(AppLayerLive)
)
// Запуск
BunRuntime.runMain(main)
// ✅ Под-программы — семантические имена
const startupSequence = pipe(
runMigrations,
Effect.flatMap(() => warmCache),
Effect.flatMap(() => registerHealthChecks)
)
const shutdownSequence = pipe(
drainConnections,
Effect.flatMap(() => flushMetrics),
Effect.flatMap(() => closeDatabase)
)
5.2 Effect.gen функции
// ✅ Внутри gen — обычные имена, yield* для получения значения
const processOrder = (orderId: OrderId) =>
Effect.gen(function* () {
const orderRepo = yield* OrderRepository
const paymentService = yield* PaymentService
const notifier = yield* NotificationService
const order = yield* orderRepo.findById(orderId)
const payment = yield* paymentService.charge(order.total)
const updatedOrder = yield* orderRepo.save({
...order,
status: "paid",
paymentId: payment.id,
})
yield* notifier.send({
userId: order.userId,
type: "order_paid",
data: { orderId: updatedOrder.id },
})
return updatedOrder
})
5.3 Fiber-именование
// ✅ Fiber — суффикс Fiber
const workerFiber: Fiber.Fiber<void, WorkerError> =
yield* Effect.fork(processQueue)
const healthCheckFiber = yield* pipe(
checkHealth,
Effect.repeat(Schedule.fixed("30 seconds")),
Effect.fork
)
// ✅ FiberId annotation
const processOrder = (id: OrderId) =>
pipe(
doProcessOrder(id),
Effect.withSpan(`processOrder:${id}`)
)
6. Типы и интерфейсы
6.1 Общие правила
// ✅ PascalCase, существительное или существительное + прилагательное
type User = { ... }
type ActiveUser = { ... }
type OrderSummary = { ... }
// ✅ Суффиксы по роли
type CreateUserInput = { ... } // входные данные
type CreateUserOutput = { ... } // выходные данные
type UserResponse = { ... } // HTTP response DTO
type UserRequest = { ... } // HTTP request DTO
type UserDTO = { ... } // Data Transfer Object
type UserRow = { ... } // строка базы данных
type UserEvent = { ... } // доменное событие
type UserState = { ... } // состояние (FSM/Reducer)
type UserAction = { ... } // действие (FSM/Reducer)
type UserCommand = { ... } // команда (CQRS)
type UserQuery = { ... } // запрос (CQRS)
type UserFilter = { ... } // фильтр для запросов
type UserPatch = { ... } // частичное обновление
type UserSnapshot = { ... } // снимок состояния
// ❌ ЗАПРЕЩЕНО
type IUser = { ... } // ❌ — I-prefix из C#/Java
type UserInterface = { ... } // ❌ — "Interface" избыточно
type UserType = { ... } // ❌ — "Type" избыточно
type TUser = { ... } // ❌ — T-prefix
6.2 Union Types и Discriminated Unions
// ✅ Enum-like union — общее имя (существительное)
type OrderStatus = "pending" | "confirmed" | "shipped" | "delivered" | "cancelled"
type PaymentMethod = "card" | "bank_transfer" | "crypto"
// ✅ Discriminated Union — суффикс по домену
type UserEvent =
| { readonly _tag: "UserCreated"; readonly user: User }
| { readonly _tag: "UserUpdated"; readonly userId: UserId; readonly changes: UserPatch }
| { readonly _tag: "UserDeleted"; readonly userId: UserId }
type PaymentResult =
| { readonly _tag: "PaymentSuccess"; readonly transactionId: string }
| { readonly _tag: "PaymentDeclined"; readonly reason: string }
| { readonly _tag: "PaymentPending"; readonly checkAfter: Duration.Duration }
// ✅ Match-паттерн для DU через pipe
const handleUserEvent = (event: UserEvent) =>
Match.value(event).pipe(
Match.tag("UserCreated", ({ user }) => sendWelcomeEmail(user)),
Match.tag("UserUpdated", ({ userId }) => invalidateCache(userId)),
Match.tag("UserDeleted", ({ userId }) => cleanupUserData(userId)),
Match.exhaustive
)
6.3 Utility Types — именование
// ✅ Readonly-обёртки (иммутабельность)
type ReadonlyUser = Readonly<User>
type DeepReadonlyConfig = Schema.Schema.Type<typeof AppConfig>
// ✅ Pick/Omit — описательные имена
type UserCredentials = Pick<User, "email" | "passwordHash">
type PublicUserInfo = Omit<User, "passwordHash" | "internalNotes">
type UserWithoutId = Omit<User, "id">
// ✅ Partial для Patch-операций
type UserPatch = Partial<Omit<User, "id" | "createdAt">>
7. Schema (Effect Schema)
7.1 Правила именования Schema
// ✅ Schema — PascalCase, имя сущности, БЕЗ суффикса Schema
// (сама Schema — это и тип, и валидатор одновременно)
const User = Schema.Struct({
id: Schema.String.pipe(Schema.brand("UserId")),
email: Schema.String.pipe(Schema.nonEmptyString(), Schema.brand("Email")),
name: Schema.String.pipe(Schema.nonEmptyString()),
age: Schema.Number.pipe(Schema.int(), Schema.positive()),
role: Schema.Literal("admin", "user", "moderator"),
createdAt: Schema.Date,
isActive: Schema.Boolean,
})
// ✅ Извлечение типа — суффикс .Type
type User = typeof User.Type
// ✅ Encoded-тип (для сериализации) — суффикс .Encoded
type UserEncoded = typeof User.Encoded
// ✅ Составные Schema — описательное имя операции
const CreateUserRequest = Schema.Struct({
email: Schema.String.pipe(Schema.nonEmptyString()),
name: Schema.String.pipe(Schema.nonEmptyString()),
password: Schema.String.pipe(Schema.minLength(8)),
})
const UpdateUserRequest = Schema.Struct({
name: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())),
age: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.positive())),
})
const UserResponse = Schema.Struct({
id: Schema.String,
email: Schema.String,
name: Schema.String,
memberSince: Schema.String,
})
// ✅ Вложенные / композитные Schema
const Address = Schema.Struct({
street: Schema.String,
city: Schema.String,
zipCode: Schema.String,
country: Schema.String,
})
const UserWithAddress = Schema.extend(User, Schema.Struct({
address: Address,
}))
// ✅ Массивы и коллекции — множественное число
const Users = Schema.Array(User)
const OrderItems = Schema.Array(OrderItem)
// ✅ Пагинация — Paginated + Entity
const PaginatedUsers = Schema.Struct({
items: Schema.Array(User),
total: Schema.Number.pipe(Schema.int()),
page: Schema.Number.pipe(Schema.int(), Schema.positive()),
pageSize: Schema.Number.pipe(Schema.int(), Schema.positive()),
})
7.2 Schema для API
// ✅ Паттерн: {Action}{Entity}{Request/Response}
const CreateUserRequest = Schema.Struct({ ... })
const CreateUserResponse = Schema.Struct({ ... })
const GetUserByIdParams = Schema.Struct({
id: Schema.String.pipe(Schema.brand("UserId")),
})
const ListUsersQuery = Schema.Struct({
page: Schema.optional(Schema.NumberFromString.pipe(Schema.int(), Schema.positive())),
pageSize: Schema.optional(Schema.NumberFromString.pipe(Schema.int(), Schema.positive())),
sortBy: Schema.optional(Schema.Literal("name", "createdAt", "email")),
sortOrder: Schema.optional(Schema.Literal("asc", "desc")),
search: Schema.optional(Schema.String),
})
// ✅ Паттерн: {Entity}{Action}Payload — для событий
const UserCreatedPayload = Schema.Struct({
userId: Schema.String.pipe(Schema.brand("UserId")),
email: Schema.String,
timestamp: Schema.Date,
})
const OrderStatusChangedPayload = Schema.Struct({
orderId: Schema.String.pipe(Schema.brand("OrderId")),
fromStatus: OrderStatusSchema,
toStatus: OrderStatusSchema,
changedBy: Schema.String.pipe(Schema.brand("UserId")),
})
8. Ошибки (Typed Errors)
8.1 Правила именования ошибок
Строгое правило: все ошибки заканчиваются на Error.
// ✅ Доменные ошибки — {Entity}{Condition}Error
export class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()(
"UserNotFoundError",
{
userId: Schema.String,
message: Schema.optional(Schema.String),
}
) {}
export class EmailAlreadyExistsError extends Schema.TaggedError<EmailAlreadyExistsError>()(
"EmailAlreadyExistsError",
{
email: Schema.String,
}
) {}
export class InsufficientBalanceError extends Schema.TaggedError<InsufficientBalanceError>()(
"InsufficientBalanceError",
{
required: Schema.Number,
available: Schema.Number,
}
) {}
// ✅ Инфраструктурные ошибки — {System}{Action}Error
export class DatabaseConnectionError extends Schema.TaggedError<DatabaseConnectionError>()(
"DatabaseConnectionError",
{
host: Schema.String,
port: Schema.Number,
cause: Schema.optional(Schema.Unknown),
}
) {}
export class HttpRequestError extends Schema.TaggedError<HttpRequestError>()(
"HttpRequestError",
{
url: Schema.String,
method: Schema.String,
statusCode: Schema.optional(Schema.Number),
}
) {}
export class CacheWriteError extends Schema.TaggedError<CacheWriteError>()(
"CacheWriteError",
{
key: Schema.String,
cause: Schema.Unknown,
}
) {}
// ✅ Бизнес-ошибки — {Process}{Reason}Error
export class PaymentDeclinedError extends Schema.TaggedError<PaymentDeclinedError>()(
"PaymentDeclinedError",
{
reason: Schema.Literal("insufficient_funds", "card_expired", "fraud_detected"),
orderId: Schema.String,
}
) {}
export class OrderCancellationNotAllowedError extends Schema.TaggedError<OrderCancellationNotAllowedError>()(
"OrderCancellationNotAllowedError",
{
orderId: Schema.String,
currentStatus: Schema.String,
}
) {}
8.2 Группировка ошибок
// ✅ Union-тип ошибок модуля — {Module}Error
type UserModuleError =
| UserNotFoundError
| EmailAlreadyExistsError
| InvalidUserDataError
type PaymentModuleError =
| PaymentDeclinedError
| PaymentTimeoutError
| PaymentProviderUnavailableError
type AppError =
| UserModuleError
| PaymentModuleError
| DatabaseConnectionError
8.3 Иерархия ошибок
{Domain}Error — корневая ошибка домена
├── {Entity}NotFoundError — сущность не найдена
├── {Entity}AlreadyExistsError — дубликат
├── {Entity}ValidationError — невалидные данные
├── {Action}NotAllowedError — запрещённое действие
├── {Action}FailedError — не удалось выполнить
├── {Resource}UnavailableError — ресурс недоступен
├── {Resource}TimeoutError — таймаут
├── {Resource}ConnectionError — ошибка соединения
├── {Process}DeclinedError — отказ (бизнес-правило)
├── {Auth}UnauthorizedError — не авторизован
├── {Auth}ForbiddenError — нет прав
├── {Data}ParseError — ошибка парсинга
├── {Data}SerializationError — ошибка сериализации
├── {Config}MissingError — отсутствует конфигурация
├── {Config}InvalidError — невалидная конфигурация
└── {Quota}ExceededError — превышение лимита/квоты
9. Сервисы
9.1 Context.Tag — определение сервиса
// ✅ Паттерн: interface + const с одинаковым именем (companion pattern)
// Interface описывает контракт, const — это Tag для DI
// Определение интерфейса сервиса
export interface UserService {
readonly findById: (id: UserId) => Effect.Effect<User, UserNotFoundError>
readonly findByEmail: (email: Email) => Effect.Effect<User, UserNotFoundError>
readonly create: (input: CreateUserInput) => Effect.Effect<User, EmailAlreadyExistsError>
readonly update: (id: UserId, patch: UserPatch) => Effect.Effect<User, UserNotFoundError>
readonly delete: (id: UserId) => Effect.Effect<void, UserNotFoundError>
readonly list: (filter: UserFilter) => Effect.Effect<PaginatedUsers>
}
// Tag с тем же именем
export const UserService = Context.GenericTag<UserService>("UserService")
// ✅ Другие примеры сервисов
export interface EmailService {
readonly send: (params: SendEmailParams) => Effect.Effect<void, EmailDeliveryError>
readonly sendBatch: (params: ReadonlyArray<SendEmailParams>) => Effect.Effect<void, EmailDeliveryError>
readonly verify: (token: string) => Effect.Effect<Email, InvalidTokenError>
}
export const EmailService = Context.GenericTag<EmailService>("EmailService")
export interface CacheService {
readonly get: <A>(key: string) => Effect.Effect<Option.Option<A>>
readonly set: <A>(key: string, value: A, ttl?: Duration.DurationInput) => Effect.Effect<void>
readonly delete: (key: string) => Effect.Effect<void>
readonly has: (key: string) => Effect.Effect<boolean>
}
export const CacheService = Context.GenericTag<CacheService>("CacheService")
9.2 Именование методов сервиса
| Операция | Имя метода | Возвращает |
|---|---|---|
| Получить один | findById, findByEmail | Effect<A, NotFound> |
| Получить все | list, findAll | Effect<Array<A>> |
| Создать | create | Effect<A, AlreadyExists> |
| Обновить | update | Effect<A, NotFound> |
| Удалить | delete, remove | Effect<void, NotFound> |
| Проверить | exists, verify | Effect<boolean> |
| Считать | count | Effect<number> |
| Поиск | search, query | Effect<Array<A>> |
| Массовые | createBatch, deleteBatch | Effect<Array<A>> |
10. Репозитории
10.1 Repository Pattern в Effect
// ✅ Интерфейс — {Entity}Repository
export interface UserRepository {
// Queries
readonly findById: (id: UserId) => Effect.Effect<User, UserNotFoundError>
readonly findByEmail: (email: Email) => Effect.Effect<Option.Option<User>>
readonly findAll: (filter: UserFilter) => Effect.Effect<ReadonlyArray<User>>
readonly count: (filter: UserFilter) => Effect.Effect<number>
readonly exists: (id: UserId) => Effect.Effect<boolean>
// Commands
readonly insert: (user: User) => Effect.Effect<User, EmailAlreadyExistsError>
readonly update: (id: UserId, patch: UserPatch) => Effect.Effect<User, UserNotFoundError>
readonly delete: (id: UserId) => Effect.Effect<void, UserNotFoundError>
readonly insertBatch: (users: ReadonlyArray<User>) => Effect.Effect<ReadonlyArray<User>>
}
export const UserRepository = Context.GenericTag<UserRepository>("UserRepository")
10.2 Реализации репозитория
Строгое правило: суффикс реализации = технология / стратегия.
// ✅ Production — суффикс технологии или Live
export const UserRepositoryPostgres: Layer.Layer<UserRepository, never, DatabaseClient> = ...
export const UserRepositorySurrealDb: Layer.Layer<UserRepository, never, SurrealClient> = ...
export const UserRepositoryLive: Layer.Layer<UserRepository, never, DatabaseClient> = ... // когда технология одна
// ✅ Тестовые — суффикс стратегии
export const UserRepositoryInMemory: Layer.Layer<UserRepository> = ...
export const UserRepositoryStub: Layer.Layer<UserRepository> = ... // фиксированные данные
export const UserRepositoryFailing: Layer.Layer<UserRepository> = ... // всегда ошибка (для тестов)
// ✅ Файлы реализаций
// UserRepository.ts — интерфейс + Tag
// UserRepositoryLive.ts — production-реализация
// UserRepositoryInMemory.ts — тестовая реализация
11. Слои (Layer)
11.1 Правила именования Layer
// ✅ Паттерн: {Service}{Implementation}
// Layer содержит реализацию сервиса, его имя совпадает с реализацией
// Простые Layer
const UserRepositoryLive: Layer.Layer<UserRepository, never, DatabaseClient> =
Layer.effect(
UserRepository,
Effect.gen(function* () {
const db = yield* DatabaseClient
return { /* implementation */ }
})
)
// ✅ Составные Layer — {Module}Live / {Feature}Live
const UserModuleLive: Layer.Layer<
UserRepository | UserService | UserCache,
never,
DatabaseClient | CacheService
> = Layer.mergeAll(
UserRepositoryLive,
UserServiceLive,
UserCacheLive,
)
// ✅ Полный граф приложения — AppLive / AppLayer
const AppLive: Layer.Layer<AppDependencies> = pipe(
Layer.mergeAll(
UserModuleLive,
OrderModuleLive,
PaymentModuleLive,
),
Layer.provideMerge(InfrastructureLive),
Layer.provideMerge(ConfigLive),
)
// ✅ Тестовые Layer — {Module}Test
const UserModuleTest: Layer.Layer<UserRepository | UserService> =
Layer.mergeAll(
UserRepositoryInMemory,
UserServiceLive,
)
11.2 Layer-файлы
src/
├── layers/
│ ├── AppLive.ts # Главный Layer приложения
│ ├── InfrastructureLive.ts # БД, кеш, очереди
│ ├── ConfigLive.ts # Конфигурация
│ └── ObservabilityLive.ts # Логирование, трейсинг, метрики
12. Доменные сущности и Value Objects
12.1 Entity
// ✅ Entity — существительное, PascalCase
// Определяется через Schema для runtime-валидации
const User = Schema.Struct({
id: UserId,
email: Email,
name: UserName,
role: UserRole,
createdAt: Schema.Date,
updatedAt: Schema.Date,
})
type User = typeof User.Type
const Order = Schema.Struct({
id: OrderId,
userId: UserId,
items: Schema.Array(OrderItem),
status: OrderStatus,
total: Money,
createdAt: Schema.Date,
})
type Order = typeof Order.Type
12.2 Value Object
// ✅ Value Object — описательное имя, всегда через Schema + Brand
const Email = Schema.String.pipe(
Schema.nonEmptyString(),
Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
Schema.brand("Email"),
)
type Email = typeof Email.Type
const Money = Schema.Struct({
amount: Schema.Number.pipe(Schema.finite()),
currency: Schema.Literal("USD", "EUR", "RUB"),
})
type Money = typeof Money.Type
const DateRange = Schema.Struct({
from: Schema.Date,
to: Schema.Date,
}).pipe(
Schema.filter(({ from, to }) => from <= to, {
message: () => "from must be before to"
})
)
type DateRange = typeof DateRange.Type
const Percentage = Schema.Number.pipe(
Schema.greaterThanOrEqualTo(0),
Schema.lessThanOrEqualTo(100),
Schema.brand("Percentage"),
)
type Percentage = typeof Percentage.Type
const NonNegativeInt = Schema.Number.pipe(
Schema.int(),
Schema.nonNegative(),
Schema.brand("NonNegativeInt"),
)
type NonNegativeInt = typeof NonNegativeInt.Type
12.3 Domain Events
// ✅ Паттерн: {Entity}{PastTenseVerb}
const UserCreated = Schema.TaggedStruct("UserCreated", {
userId: UserId,
email: Email,
occurredAt: Schema.Date,
})
type UserCreated = typeof UserCreated.Type
const OrderShipped = Schema.TaggedStruct("OrderShipped", {
orderId: OrderId,
trackingNumber: Schema.String,
carrier: Schema.String,
shippedAt: Schema.Date,
})
type OrderShipped = typeof OrderShipped.Type
const PaymentReceived = Schema.TaggedStruct("PaymentReceived", {
orderId: OrderId,
amount: Money,
method: PaymentMethod,
receivedAt: Schema.Date,
})
type PaymentReceived = typeof PaymentReceived.Type
// ✅ Union всех событий домена
type DomainEvent =
| UserCreated
| UserUpdated
| UserDeleted
| OrderCreated
| OrderShipped
| PaymentReceived
13. Branded Types
13.1 Правила
// ✅ Brand-строка = PascalCase, совпадает с именем типа
// Branded type — это тег компилятора для type safety
const UserId = Schema.String.pipe(
Schema.nonEmptyString(),
Schema.brand("UserId")
)
type UserId = typeof UserId.Type
const OrderId = Schema.String.pipe(
Schema.nonEmptyString(),
Schema.brand("OrderId")
)
type OrderId = typeof OrderId.Type
const ProductId = Schema.String.pipe(
Schema.nonEmptyString(),
Schema.brand("ProductId")
)
type ProductId = typeof ProductId.Type
// ✅ Branded числа
const PositiveInt = Schema.Number.pipe(
Schema.int(),
Schema.positive(),
Schema.brand("PositiveInt")
)
type PositiveInt = typeof PositiveInt.Type
const Port = Schema.Number.pipe(
Schema.int(),
Schema.greaterThanOrEqualTo(1),
Schema.lessThanOrEqualTo(65535),
Schema.brand("Port")
)
type Port = typeof Port.Type
// ✅ Branded-строки с валидацией
const Url = Schema.String.pipe(
Schema.pattern(/^https?:\/\/.+/),
Schema.brand("Url")
)
type Url = typeof Url.Type
const JwtToken = Schema.String.pipe(
Schema.nonEmptyString(),
Schema.brand("JwtToken")
)
type JwtToken = typeof JwtToken.Type
// ❌ ЗАПРЕЩЕНО
type UserId = string & Brand.Brand<"userId"> // ❌ — бренд в camelCase
type ID = string & Brand.Brand<"ID"> // ❌ — абстрактное имя
14. Pipe, Flow и композиция
14.1 Pipeline-именование
// ✅ Промежуточные результаты в pipe — описательные имена
const processRegistration = (input: RegistrationInput) =>
pipe(
validateRegistration(input), // Effect<ValidatedInput, ValidationError>
Effect.flatMap(normalizeRegistrationData), // Effect<NormalizedInput, ValidationError>
Effect.flatMap(checkEmailUniqueness), // Effect<NormalizedInput, EmailAlreadyExistsError>
Effect.flatMap(createUserFromRegistration), // Effect<User, CreateUserError>
Effect.tap(sendVerificationEmail), // Effect<User, EmailDeliveryError>
Effect.tap(publishUserCreatedEvent), // Effect<User, EventPublishError>
Effect.map(toRegistrationResponse), // Effect<RegistrationResponse, ...>
)
// ✅ Flow — именование как функция, описывает полный pipeline
const processPayment: (input: PaymentInput) => Effect.Effect<PaymentResult, PaymentError, PaymentDependencies> =
flow(
validatePaymentInput,
Effect.flatMap(resolvePaymentMethod),
Effect.flatMap(executePayment),
Effect.flatMap(recordTransaction),
)
14.2 Лямбды в pipe
// ✅ Короткие лямбды — 1 параметр, без скобок
pipe(users, ReadonlyArray.map((u) => u.name))
pipe(users, ReadonlyArray.filter((u) => u.isActive))
pipe(numbers, ReadonlyArray.reduce(0, (acc, n) => acc + n))
// ✅ Ссылки на функции вместо лямбд (point-free стиль)
pipe(users, ReadonlyArray.map(getFullName))
pipe(users, ReadonlyArray.filter(isActive))
pipe(userIds, ReadonlyArray.map(findUserById))
// ❌ ЗАПРЕЩЕНО: бессмысленные имена в лямбдах
pipe(users, ReadonlyArray.map((x) => x.name)) // ❌ — "x" не информативен
pipe(users, ReadonlyArray.map((item) => item.name)) // ⚠️ — "item" слишком обобщённо
pipe(users, ReadonlyArray.map((data) => data.name)) // ❌ — "data" бессмысленно
15. Stream, Queue, Pub/Sub
15.1 Stream
// ✅ Stream-переменные — описание потока данных
const userActivityStream: Stream.Stream<UserActivity, DatabaseError, DatabaseClient> =
pipe(
Stream.fromEffect(fetchRecentActivity),
Stream.flatMap((activities) => Stream.fromIterable(activities)),
)
const logStream: Stream.Stream<LogEntry> = ...
const priceUpdateStream: Stream.Stream<PriceUpdate, WebSocketError> = ...
const orderEventStream: Stream.Stream<OrderEvent, never, EventBus> = ...
// ✅ Stream-функции — глагол + "Stream" или описательное имя
const streamUserEvents = (userId: UserId): Stream.Stream<UserEvent> => ...
const watchPriceChanges = (productId: ProductId): Stream.Stream<PriceUpdate> => ...
const tailLogFile = (path: string): Stream.Stream<string, FileError> => ...
15.2 Queue
// ✅ Queue-переменные — суффикс Queue
const taskQueue: Queue.Queue<Task> = ...
const notificationQueue: Queue.Queue<Notification> = ...
const deadLetterQueue: Queue.Queue<FailedMessage> = ...
// ✅ Dequeue/Enqueue — описание направления
const incomingMessages: Queue.Enqueue<Message> = ...
const processedResults: Queue.Dequeue<Result> = ...
15.3 PubSub / EventBus
// ✅ Topic/Channel — описательное имя
const userEventsTopic: PubSub.PubSub<UserEvent> = ...
const orderUpdatesChannel: PubSub.PubSub<OrderUpdate> = ...
// ✅ Паттерн: {domain}Events{Topic/Channel}
const paymentEventsChannel = yield* PubSub.unbounded<PaymentEvent>()
16. Config и Environment
16.1 Config-схемы
// ✅ Паттерн: {Module}Config
const DatabaseConfig = Schema.Struct({
host: Schema.String,
port: Port,
database: Schema.String,
username: Schema.String,
password: Schema.Redacted(Schema.String),
maxConnections: Schema.optional(PositiveInt).pipe(
Schema.withDefault(() => PositiveInt.make(10))
),
connectionTimeoutMs: Schema.optional(Schema.Number).pipe(
Schema.withDefault(() => 5000)
),
})
type DatabaseConfig = typeof DatabaseConfig.Type
const HttpServerConfig = Schema.Struct({
port: Port,
host: Schema.String,
corsOrigins: Schema.Array(Schema.String),
requestTimeoutMs: Schema.Number,
})
type HttpServerConfig = typeof HttpServerConfig.Type
const AppConfig = Schema.Struct({
database: DatabaseConfig,
http: HttpServerConfig,
logLevel: Schema.Literal("debug", "info", "warn", "error"),
environment: Schema.Literal("development", "staging", "production"),
})
type AppConfig = typeof AppConfig.Type
16.2 Config Tag
// ✅ Config как сервис
const AppConfig = Context.GenericTag<AppConfig>("AppConfig")
const DatabaseConfig = Context.GenericTag<DatabaseConfig>("DatabaseConfig")
// ✅ Config Layer
const AppConfigLive: Layer.Layer<AppConfig> = Layer.effect(
AppConfig,
Effect.gen(function* () {
const raw = yield* Effect.config(Config.string("APP_CONFIG_PATH"))
// ... load and parse
})
)
16.3 Environment Variables
// ✅ UPPER_SNAKE_CASE, префикс по модулю/приложению
// APP_ — общие приложения
// DB_ — база данных
// HTTP_ — HTTP сервер
// AUTH_ — аутентификация
// CACHE_ — кеш
const envConfig = {
APP_ENV: Config.string("APP_ENV"),
APP_LOG_LEVEL: Config.string("APP_LOG_LEVEL"),
DB_HOST: Config.string("DB_HOST"),
DB_PORT: Config.integer("DB_PORT"),
DB_PASSWORD: Config.redacted("DB_PASSWORD"),
HTTP_PORT: Config.integer("HTTP_PORT"),
AUTH_JWT_SECRET: Config.redacted("AUTH_JWT_SECRET"),
AUTH_TOKEN_TTL_SECONDS: Config.integer("AUTH_TOKEN_TTL_SECONDS"),
CACHE_REDIS_URL: Config.string("CACHE_REDIS_URL"),
} as const
17. HTTP
17.1 HttpRouter и Routes
// ✅ Паттерн: {Entity}Routes / {Entity}Router
const UserRoutes = HttpRouter.empty.pipe(
HttpRouter.get("/users", listUsersHandler),
HttpRouter.get("/users/:id", getUserByIdHandler),
HttpRouter.post("/users", createUserHandler),
HttpRouter.patch("/users/:id", updateUserHandler),
HttpRouter.del("/users/:id", deleteUserHandler),
)
const OrderRoutes = HttpRouter.empty.pipe(
HttpRouter.get("/orders", listOrdersHandler),
HttpRouter.get("/orders/:id", getOrderByIdHandler),
HttpRouter.post("/orders", createOrderHandler),
)
// ✅ Главный роутер — AppRouter / ApiRouter
const ApiRouter = HttpRouter.empty.pipe(
HttpRouter.mount("/api/v1", pipe(
HttpRouter.empty,
HttpRouter.mount("/users", UserRoutes),
HttpRouter.mount("/orders", OrderRoutes),
HttpRouter.mount("/payments", PaymentRoutes),
)),
)
// ✅ Health/System роутер
const SystemRouter = HttpRouter.empty.pipe(
HttpRouter.get("/health", healthHandler),
HttpRouter.get("/ready", readinessHandler),
HttpRouter.get("/metrics", metricsHandler),
)
17.2 Handlers
// ✅ Паттерн: {action}{Entity}Handler
const listUsersHandler = HttpRouter.makeHandler(
Effect.gen(function* () {
const userService = yield* UserService
const users = yield* userService.list({})
return HttpServerResponse.json(users)
})
)
const getUserByIdHandler = HttpRouter.makeHandler(
Effect.gen(function* () {
const params = yield* HttpRouter.params
const userService = yield* UserService
const user = yield* userService.findById(UserId(params.id))
return HttpServerResponse.json(toUserResponse(user))
})
)
const createUserHandler = HttpRouter.makeHandler(
Effect.gen(function* () {
const body = yield* HttpServerRequest.schemaBodyJson(CreateUserRequest)
const userService = yield* UserService
const user = yield* userService.create(body)
return HttpServerResponse.json(toUserResponse(user), { status: 201 })
})
)
18. RPC
18.1 RPC-определения
// ✅ Паттерн: {Action}{Entity} — имя запроса как действие
const GetUser = Rpc.make("GetUser", {
payload: Schema.Struct({ id: UserId }),
success: User,
error: UserNotFoundError,
})
const CreateUser = Rpc.make("CreateUser", {
payload: CreateUserRequest,
success: User,
error: Schema.Union(EmailAlreadyExistsError, InvalidUserDataError),
})
const ListUsers = Rpc.make("ListUsers", {
payload: ListUsersQuery,
success: PaginatedUsers,
error: Schema.Never,
})
// ✅ Группа RPC — {Module}Rpcs
const UserRpcs = RpcGroup.make("UserRpcs", {
GetUser,
CreateUser,
ListUsers,
UpdateUser,
DeleteUser,
})
// ✅ Реализация — {Module}RpcsLive
const UserRpcsLive = UserRpcs.toLayer(
Effect.gen(function* () {
const userService = yield* UserService
return {
GetUser: ({ id }) => userService.findById(id),
CreateUser: (input) => userService.create(input),
ListUsers: (query) => userService.list(query),
UpdateUser: ({ id, ...patch }) => userService.update(id, patch),
DeleteUser: ({ id }) => userService.delete(id),
}
})
)
19. Middleware
19.1 Именование Middleware
// ✅ Паттерн: {Concern}Middleware
const AuthMiddleware = HttpMiddleware.make((app) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
const token = extractBearerToken(request)
const user = yield* verifyToken(token)
return yield* app.pipe(
Effect.provideService(CurrentUser, user)
)
})
)
const LoggingMiddleware = HttpMiddleware.make((app) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
const start = yield* Clock.currentTimeMillis
const response = yield* app
const duration = (yield* Clock.currentTimeMillis) - start
yield* Effect.log(`${request.method} ${request.url} ${response.status} ${duration}ms`)
return response
})
)
const CorsMiddleware = HttpMiddleware.make((app) => ...)
const RateLimitMiddleware = HttpMiddleware.make((app) => ...)
const CompressionMiddleware = HttpMiddleware.make((app) => ...)
const TimeoutMiddleware = HttpMiddleware.make((app) => ...)
const ErrorBoundaryMiddleware = HttpMiddleware.make((app) => ...)
const TracingMiddleware = HttpMiddleware.make((app) => ...)
const CacheControlMiddleware = HttpMiddleware.make((app) => ...)
// ✅ Composition — pipe
const MiddlewareStack = flow(
ErrorBoundaryMiddleware,
LoggingMiddleware,
TracingMiddleware,
CorsMiddleware,
AuthMiddleware,
RateLimitMiddleware,
)
20. Тесты
20.1 Именование тестов
// ✅ describe — модуль/функция
// ✅ it — поведение на естественном языке
describe("UserService", () => {
describe("create", () => {
it("should create a user with valid input", () =>
Effect.gen(function* () {
const service = yield* UserService
const user = yield* service.create(validInput)
expect(user.email).toBe(validInput.email)
}).pipe(
Effect.provide(UserModuleTest),
Effect.runPromise
)
)
it("should fail with EmailAlreadyExistsError for duplicate email", () =>
Effect.gen(function* () {
const service = yield* UserService
yield* service.create(validInput)
const result = yield* service.create(validInput).pipe(Effect.either)
expect(Either.isLeft(result)).toBe(true)
}).pipe(
Effect.provide(UserModuleTest),
Effect.runPromise
)
)
it("should hash password before storing", () => ...)
it("should publish UserCreated event", () => ...)
it("should set default role to 'user'", () => ...)
})
})
20.2 Тестовые утилиты
// ✅ Паттерн: make{Entity}Fixture / fake{Entity} / stub{Entity}
const makeUserFixture = (overrides?: Partial<User>): User => ({
id: UserId("test-user-id"),
email: Email("test@example.com"),
name: "Test User",
role: "user",
isActive: true,
createdAt: new Date("2024-01-01"),
updatedAt: new Date("2024-01-01"),
...overrides,
})
const makeOrderFixture = (overrides?: Partial<Order>): Order => ({
id: OrderId("test-order-id"),
userId: UserId("test-user-id"),
items: [],
status: "pending",
total: { amount: 0, currency: "USD" },
createdAt: new Date("2024-01-01"),
...overrides,
})
// ✅ Генераторы — generate{Entity}
const generateUserId = (): UserId => UserId(crypto.randomUUID())
const generateEmail = (prefix = "test"): Email =>
Email(`${prefix}-${crypto.randomUUID().slice(0, 8)}@example.com`)
21. Файлы и модули
21.1 Правила именования файлов
| Содержимое | Стиль файла | Пример |
|---|---|---|
| Entity / Schema | PascalCase | User.ts, Order.ts |
| Service interface + Tag | PascalCase | UserService.ts |
| Service implementation | PascalCase | UserServiceLive.ts |
| Repository interface + Tag | PascalCase | UserRepository.ts |
| Repository implementation | PascalCase | UserRepositoryLive.ts |
| Error definitions | PascalCase | UserErrors.ts |
| Layer composition | PascalCase | AppLive.ts |
| Pure utility function | camelCase | hashPassword.ts |
| Helper / shared utility | camelCase | formatDate.ts |
| Constants | camelCase | httpStatus.ts |
| Config schema | PascalCase | AppConfig.ts |
| HTTP Routes | PascalCase | UserRoutes.ts |
| HTTP Handlers | PascalCase | UserHandlers.ts |
| Middleware | PascalCase | AuthMiddleware.ts |
| Test file | PascalCase | UserService.test.ts |
| Index / barrel export | camelCase | index.ts |
21.2 Один файл — одна ответственность
// ✅ Файл UserRepository.ts — ТОЛЬКО интерфейс и Tag
export interface UserRepository { ... }
export const UserRepository = Context.GenericTag<UserRepository>("UserRepository")
// ✅ Файл UserRepositoryLive.ts — ТОЛЬКО production-реализация
export const UserRepositoryLive: Layer.Layer<UserRepository, never, DatabaseClient> = ...
// ✅ Файл UserErrors.ts — ВСЕ ошибки модуля
export class UserNotFoundError extends ...
export class EmailAlreadyExistsError extends ...
export class InvalidUserDataError extends ...
export type UserModuleError = UserNotFoundError | EmailAlreadyExistsError | InvalidUserDataError
// ✅ Файл index.ts — barrel export
export { UserService } from "./UserService.js"
export type { UserService as UserServiceType } from "./UserService.js"
export { UserServiceLive } from "./UserServiceLive.js"
export { UserRepository } from "./UserRepository.js"
export { UserRepositoryLive } from "./UserRepositoryLive.js"
export * from "./UserErrors.js"
export { User, CreateUserRequest, UserResponse } from "./schemas/index.js"
22. Директории и архитектура проекта
22.1 Feature-Sliced / Hexagonal
src/
├── modules/ # Бизнес-модули (kebab-case)
│ ├── user-management/ # Домен: управление пользователями
│ │ ├── domain/ # Чистый домен (нет зависимостей)
│ │ │ ├── entities/ # Сущности
│ │ │ │ └── User.ts
│ │ │ ├── value-objects/ # Value Objects
│ │ │ │ ├── Email.ts
│ │ │ │ ├── UserId.ts
│ │ │ │ └── UserName.ts
│ │ │ ├── events/ # Доменные события
│ │ │ │ ├── UserCreated.ts
│ │ │ │ └── UserUpdated.ts
│ │ │ └── errors/ # Доменные ошибки
│ │ │ └── UserErrors.ts
│ │ │
│ │ ├── application/ # Use Cases / Application Logic
│ │ │ ├── ports/ # Порты (интерфейсы)
│ │ │ │ ├── UserRepository.ts
│ │ │ │ └── UserNotifier.ts
│ │ │ ├── services/ # Application Services
│ │ │ │ └── UserService.ts
│ │ │ ├── commands/ # Command Handlers (CQRS)
│ │ │ │ ├── CreateUser.ts
│ │ │ │ └── UpdateUser.ts
│ │ │ └── queries/ # Query Handlers (CQRS)
│ │ │ ├── GetUserById.ts
│ │ │ └── ListUsers.ts
│ │ │
│ │ ├── infrastructure/ # Реализации портов
│ │ │ ├── UserRepositoryLive.ts
│ │ │ ├── UserRepositoryInMemory.ts
│ │ │ └── UserNotifierLive.ts
│ │ │
│ │ ├── adapters/ # Внешние интерфейсы
│ │ │ ├── http/
│ │ │ │ ├── UserRoutes.ts
│ │ │ │ └── UserHandlers.ts
│ │ │ └── rpc/
│ │ │ └── UserRpcs.ts
│ │ │
│ │ └── index.ts # Public API модуля
│ │
│ ├── order-processing/
│ │ └── ...
│ │
│ └── payment-gateway/
│ └── ...
│
├── shared/ # Общие утилиты
│ ├── kernel/ # Shared Kernel (общие типы)
│ │ ├── branded-types.ts # UserId, Email, etc.
│ │ ├── pagination.ts # Paginated<T>
│ │ └── money.ts # Money value object
│ ├── errors/ # Общие ошибки
│ │ └── infrastructure-errors.ts
│ ├── utils/ # Чистые утилиты
│ │ ├── date.ts
│ │ └── string.ts
│ └── middleware/ # Общие middleware
│ ├── AuthMiddleware.ts
│ ├── LoggingMiddleware.ts
│ └── CorsMiddleware.ts
│
├── infrastructure/ # Глобальная инфраструктура
│ ├── database/
│ │ ├── DatabaseClient.ts # Tag + interface
│ │ ├── DatabaseClientLive.ts # Production
│ │ └── migrations/
│ ├── cache/
│ │ ├── CacheService.ts
│ │ └── CacheServiceRedis.ts
│ ├── messaging/
│ │ ├── EventBus.ts
│ │ └── EventBusLive.ts
│ └── observability/
│ ├── TracingLive.ts
│ ├── MetricsLive.ts
│ └── LoggingLive.ts
│
├── layers/ # Композиция Layer
│ ├── AppLive.ts
│ ├── InfrastructureLive.ts
│ └── AppTest.ts
│
├── config/ # Конфигурация
│ ├── AppConfig.ts
│ └── ConfigLive.ts
│
└── main.ts # Entry point
23. Generics
23.1 Правила параметров типов
| Параметр | Семантика | Где используется |
|---|---|---|
A | Success type | Effect<A, E, R>, Option<A> |
E | Error type | Effect<A, E, R> |
R | Requirements/Context | Effect<A, E, R>, Layer<A, E, R> |
B | Второй success type | map, flatMap |
Self | Рекурсивная ссылка | Schema.Class, tagged errors |
I | Input type | Трансформации |
O | Output type | Трансформации |
K | Key type | Map<K, V>, Record<K, V> |
V | Value type | Map<K, V>, Record<K, V> |
T | Произвольный тип | Generic-утилиты (последний выбор) |
S | State type | Reducer, Machine |
// ✅ В реальном коде — описательные имена для сложных generic
const makeRepository = <
Entity,
CreateInput,
EntityId,
FindError
>(config: {
readonly tableName: string
readonly idField: keyof Entity
readonly schema: Schema.Schema<Entity>
}): {
readonly findById: (id: EntityId) => Effect.Effect<Entity, FindError>
readonly create: (input: CreateInput) => Effect.Effect<Entity>
} => ...
// ✅ Effect-стиль — A, E, R для Effect-подобных типов
const retry = <A, E, R>(
self: Effect.Effect<A, E, R>,
schedule: Schedule.Schedule<unknown, E>
): Effect.Effect<A, E, R> => ...
24. Ref и состояние
24.1 Ref
// ✅ Ref-переменные — суффикс Ref
const counterRef: Ref.Ref<number> = yield* Ref.make(0)
const stateRef: Ref.Ref<AppState> = yield* Ref.make(initialState)
const usersRef: Ref.Ref<ReadonlyMap<UserId, User>> = yield* Ref.make(new Map())
const configRef: Ref.Ref<AppConfig> = yield* Ref.make(defaultConfig)
// ✅ SynchronizedRef — суффикс SyncRef
const cacheSyncRef: SynchronizedRef.SynchronizedRef<Cache> =
yield* SynchronizedRef.make(emptyCache)
// ✅ FiberRef — описание контекста
const currentUserRef: FiberRef.FiberRef<Option.Option<User>> =
FiberRef.unsafeMake(Option.none())
const requestIdRef: FiberRef.FiberRef<string> =
FiberRef.unsafeMake("unknown")
const logContextRef: FiberRef.FiberRef<ReadonlyRecord<string, string>> =
FiberRef.unsafeMake({})
24.2 Операции с Ref
// ✅ Функции работы с Ref — стандартные глаголы
const incrementCounter = Ref.update(counterRef, (n) => n + 1)
const getCounter = Ref.get(counterRef)
const resetCounter = Ref.set(counterRef, 0)
const addUser = (user: User) =>
Ref.update(usersRef, (map) =>
ReadonlyMap.set(map, user.id, user)
)
25. Resource Management
25.1 Scope и Finalizer
// ✅ Scoped ресурсы — acquire/release глаголы
const acquireDatabaseConnection = Effect.acquireRelease(
connectToDatabase(config), // acquire
(connection) => connection.close() // release
)
const acquireFileHandle = (path: string) =>
Effect.acquireRelease(
openFile(path),
(handle) => closeFile(handle)
)
// ✅ Scoped-сервисы — суффикс Scoped
const makeScopedHttpClient = Effect.acquireRelease(
createHttpClient(config),
(client) => client.shutdown()
)
// ✅ Pool — суффикс Pool
const connectionPool = Pool.make({
acquire: connectToDatabase(config),
size: 10,
})
type ConnectionPool = Pool.Pool<DatabaseConnection, DatabaseConnectionError>
26. Schedule, Duration, Cron
26.1 Schedule
// ✅ Schedule-переменные — описание политики
const retryPolicy = pipe(
Schedule.exponential("100 millis"),
Schedule.intersect(Schedule.recurs(5)),
Schedule.jittered
)
const pollingSchedule = Schedule.fixed("5 seconds")
const backoffSchedule = pipe(
Schedule.exponential("1 second", 2),
Schedule.either(Schedule.spaced("30 seconds")),
)
const healthCheckSchedule = Schedule.fixed("30 seconds")
const cacheRefreshSchedule = Schedule.fixed("5 minutes")
const metricsFlushSchedule = Schedule.fixed("10 seconds")
const tokenRotationSchedule = Schedule.fixed("1 hour")
26.2 Duration
// ✅ Duration-переменные — описание назначения
const requestTimeout = Duration.seconds(30)
const connectionTimeout = Duration.seconds(5)
const idleTimeout = Duration.minutes(15)
const sessionTtl = Duration.hours(24)
const cacheTtl = Duration.minutes(5)
const lockTimeout = Duration.seconds(10)
const gracefulShutdownTimeout = Duration.seconds(30)
27. Метрики и трейсинг
27.1 Метрики
// ✅ Паттерн: {module}.{metric_type}.{name} (snake_case через точки)
const httpRequestDuration = Metric.histogram("http.request.duration_ms", {
description: "HTTP request duration in milliseconds",
boundaries: [10, 25, 50, 100, 250, 500, 1000, 2500, 5000],
})
const httpRequestTotal = Metric.counter("http.request.total", {
description: "Total number of HTTP requests",
})
const activeConnectionsGauge = Metric.gauge("db.connections.active", {
description: "Number of active database connections",
})
const orderAmountSummary = Metric.summary("order.amount_usd", {
description: "Order amounts in USD",
maxAge: Duration.hours(1),
quantiles: [0.5, 0.9, 0.95, 0.99],
})
const cacheHitRate = Metric.counter("cache.hit.total")
const cacheMissRate = Metric.counter("cache.miss.total")
27.2 Spans / Tracing
// ✅ Span-имена — {module}.{operation} (точечная нотация)
const findUser = (id: UserId) =>
pipe(
doFindUser(id),
Effect.withSpan("user.findById", {
attributes: { "user.id": id },
})
)
const processPayment = (orderId: OrderId) =>
pipe(
doProcessPayment(orderId),
Effect.withSpan("payment.process", {
attributes: { "order.id": orderId },
})
)
// ✅ Атрибуты — {domain}.{attribute} (точечная нотация, snake_case)
// user.id, user.email, order.id, order.status
// http.method, http.url, http.status_code
// db.statement, db.duration_ms
28. Anti-patterns: запрещённые практики
28.1 Именование
// ❌ ЗАПРЕЩЕНО: Manager/Handler/Processor/Helper без контекста
class UserManager { } // ❌ — что именно "управляет"?
class DataHandler { } // ❌ — какие данные?
class RequestProcessor { } // ❌ — что обрабатывает?
class StringHelper { } // ❌ — используй описательные имена функций
// ✅ ПРАВИЛЬНО: конкретные имена
class UserAuthenticationService { }
class OrderFulfillmentService { }
class EmailDeliveryService { }
// ❌ ЗАПРЕЩЕНО: абстрактные имена
const data = ... // ❌
const info = ... // ❌
const temp = ... // ❌
const result = ... // ❌ (допустимо только внутри gen)
const value = ... // ❌
const item = ... // ❌
const stuff = ... // ❌
// ❌ ЗАПРЕЩЕНО: отрицательные имена для булевых
const isNotActive = false // ❌ — двойное отрицание при проверке
const hasNoPermission = true // ❌
const cannotEdit = false // ❌
// ✅ ПРАВИЛЬНО: положительные имена
const isActive = true // if (isActive)
const hasPermission = false // if (!hasPermission)
const isEditable = true // if (isEditable)
// ❌ ЗАПРЕЩЕНО: глагол для типа
type CreateUser = { ... } // ⚠️ допустимо только для Command/Request
type FetchOrder = { ... } // ❌ — тип не действие
// ✅ ПРАВИЛЬНО
type User = { ... }
type CreateUserCommand = { ... }
type FetchOrderQuery = { ... }
// ❌ ЗАПРЕЩЕНО: одна буква (кроме лямбд и generic)
const u = getUser() // ❌
const o = createOrder() // ❌
// ✅ ПРАВИЛЬНО
const user = yield* getUser()
const order = yield* createOrder()
// ❌ ЗАПРЕЩЕНО: тавтология
type UserUser = { ... } // ❌
const getUserUser = () => ... // ❌
const userServiceService = () => ... // ❌
// ❌ ЗАПРЕЩЕНО: слово "Impl" для реализации
class UserServiceImpl { } // ❌ — используй Live/InMemory/Postgres
const UserRepositoryImpl = Layer.effect( // ❌
28.2 Структурные Anti-patterns
// ❌ ЗАПРЕЩЕНО: utils.ts / helpers.ts — файл-свалка
// src/utils.ts — ❌ всё в одном файле
// ✅ ПРАВИЛЬНО: по доменам
// src/shared/utils/date.ts
// src/shared/utils/string.ts
// src/shared/utils/crypto.ts
// ❌ ЗАПРЕЩЕНО: God-сервис (>10 методов)
interface UserService {
findById: ...
create: ...
update: ...
delete: ...
sendEmail: ... // ❌ — не ответственность UserService
generateReport: ... // ❌ — отдельный ReportService
syncWithCRM: ... // ❌ — отдельный CrmSyncService
processPayment: ... // ❌ — отдельный PaymentService
}
// ✅ ПРАВИЛЬНО: Single Responsibility
interface UserService { findById, create, update, delete, list }
interface EmailService { send, sendBatch, verify }
interface ReportService { generate, schedule }
interface CrmSyncService { sync, syncBatch }
29. Краткая шпаргалка
Мгновенная справка
ПЕРЕМЕННАЯ camelCase userId, activeUsers, maxRetries
БУЛЕВА is/has/can + Noun isActive, hasRole, canEdit
ФУНКЦИЯ ЧИСТАЯ verb + Noun createUser, formatDate, calcTotal
ФУНКЦИЯ EFFECT verb + Noun findUserById, saveOrder, sendEmail
ПРЕДИКАТ is/has/can isValid, hasPermission, canAccess
КОНСТРУКТОР make + Noun makeUser, makeDefaultConfig
HOF with + Noun withRetry, withTimeout, withLogging
ФАБРИКА make + Noun makeValidator, makeRepository
ТИП PascalCase User, OrderSummary, PaymentResult
SCHEMA PascalCase CreateUserRequest, UserResponse
ОШИБКА PascalCase + Error UserNotFoundError, PaymentDeclinedError
UNION ОШИБОК PascalCase + Error UserModuleError, AppError
BRAND PascalCase строка "UserId", "Email", "Port"
DISCRIMINATED UNION PascalCase _tag { _tag: "UserCreated" }
GENERIC A, E, R, B, K, V Effect<A, E, R>
СЕРВИС (Tag) PascalCase UserService, CacheService
РЕПОЗИТОРИЙ PascalCase UserRepository, OrderRepository
LAYER (prod) PascalCase + Live UserServiceLive, AppLive
LAYER (test) + InMemory/Test UserRepositoryInMemory, AppTest
MIDDLEWARE PascalCase + MW AuthMiddleware, LoggingMiddleware
ФАЙЛ (модуль) PascalCase.ts UserService.ts, UserErrors.ts
ФАЙЛ (утилита) camelCase.ts formatDate.ts, hashPassword.ts
ДИРЕКТОРИЯ kebab-case user-management/, shared/
ТЕСТ PascalCase.test.ts UserService.test.ts
REF name + Ref counterRef, stateRef, usersRef
FIBER name + Fiber workerFiber, healthCheckFiber
STREAM name + Stream logStream, priceUpdateStream
QUEUE name + Queue taskQueue, deadLetterQueue
SCHEDULE описательное retryPolicy, pollingSchedule
DURATION описательное requestTimeout, cacheTtl
МЕТРИКА dot.notation http.request.duration_ms
SPAN dot.notation user.findById, payment.process
ENV VARIABLE UPPER_SNAKE DB_HOST, AUTH_JWT_SECRET
CONFIG PascalCase + Config DatabaseConfig, HttpServerConfig
Правило №1: Если сомневаешься в имени — представь, что через 6 месяцев ты открыл этот файл в 3 часа ночи во время инцидента. Имя должно быть настолько очевидным, что не требует ни контекста, ни документации.
Правило №2: Консистентность важнее креативности. Лучше скучный, но предсказуемый
findUserById, чем элегантный, но неочевидныйresolveUser.
Правило №3: Effect-тип — это документация.
Effect<User, UserNotFoundError, UserRepository>рассказывает всю историю: что возвращает, как может упасть, что требует. Имя функции лишь дополняет эту историю.