Ринцвальд кладёт на стол Рыдкулла пачку листов из бумаг конторы Гилта. Сверху отчёт по башням, рабочий: контора гоняла его каждое утро до самого разоблачения. Код короткий и понятный. Только половины имён из него нет в Effect, который ты учил весь курс.
В уроке 118 ты переводил на Effect чужой код на Promise. Теперь чужой код уже на Effect, но на старом. Это проще: модель программы та же. Effect, Layer, Schema, Stream, Effect.gen и pipe никуда не делись. Поменялись имена, адреса модулей и несколько привычек.
Как узнать код v3
Вот кусок записки. Гилт собирает с башен плату за проезд писем:
// Записка Гилта, Effect 3
import { Effect, Either, Layer } from 'effect';
class Tolls extends Effect.Tag('Tolls')<
Tolls,
{ readonly charge: (tower: string) => Effect.Effect<number, TollRefused> }
>() {}
const TollsLive = Layer.scoped(
Tolls,
Effect.gen(function* () {
const ledger = yield* Effect.acquireRelease(openLedger, () => closeLedger);
return { charge: (tower) => ledger.charge(tower) };
}),
);
const collect = (towers: ReadonlyArray<string>) =>
Effect.forEach(towers, (tower) => Effect.either(Tolls.charge(tower))).pipe(
Effect.map((results) => results.filter(Either.isRight).map((r) => r.right)),
Effect.tap(() => Effect.fork(Effect.log('сбор окончен'))),
);
На 4.1 этот код не соберётся, и с первого взгляда видно почему. Приметы v3 короткие:
- сервис объявлен через
Context.Tag('...'),Effect.Tag,Effect.ServiceилиContext.GenericTag; уContext.TagиEffect.Tagстрока ключа стоит до типов; - метод сервиса зовут прямо на классе:
Tolls.charge(tower). Это аксессор, в v4 его нет; - в коде есть
Either,catchAll,fork,zipRight; - импорты из
@effect/platform,@effect/schema,@effect/rpc,@effect/clusterили@effect/cli; Layer.scopedдля слоя, который что-то открывает.
Своя примета есть у кода из бет 4.0, которые выходили с февраля 2026 года: адреса вида effect/unstable/http и, в ранних бетах, модуль ServiceMap вместо Context. Такой код ближе к 4.1, чем код v3, но тоже не соберётся.
Таблица переименований
Тот же кусок на 4.1:
// Записка Гилта, Effect 4.1
import { Context, Effect, Layer, Result } from 'effect';
class Tolls extends Context.Service<
Tolls,
{ readonly charge: (tower: string) => Effect.Effect<number, TollRefused> }
>()('Tolls') {}
const TollsLive = Layer.effect(
Tolls,
Effect.gen(function* () {
const ledger = yield* Effect.acquireRelease(openLedger, () => closeLedger);
return Tolls.of({ charge: (tower) => ledger.charge(tower) });
}),
);
const collect = (towers: ReadonlyArray<string>) =>
Effect.forEach(towers, (tower) => Effect.result(Tolls.use((tolls) => tolls.charge(tower)))).pipe(
Effect.map((results) => results.filter(Result.isSuccess).map((r) => r.success)),
Effect.tap(() => Effect.forkChild(Effect.log('сбор окончен'))),
);
Логика не поменялась ни на строку, поменялись только имена. Вот те, что встречаются чаще всего; каждое проверено на effect@4.1.0:
| В коде v3 | В 4.1 |
|---|---|
Context.Tag('Id')<Self, Shape>() | Context.Service<Self, Shape>()('Id') |
Context.GenericTag<T>('Id') | Context.Service<T>('Id') |
Effect.Tag, Effect.Service | Context.Service; слой пишешь сам через Layer.effect |
аксессор Tolls.charge(x) | Tolls.use((tolls) => tolls.charge(x)) |
Either, Either.right, Either.isRight | Result, Result.succeed, Result.isSuccess |
Effect.either | Effect.result |
Effect.catchAll, catchAllCause, catchAllDefect | Effect.catch, catchCause, catchDefect |
Effect.catchSome | Effect.catchFilter |
Effect.fork, Effect.forkDaemon | Effect.forkChild, Effect.forkDetach |
Effect.zipRight | Effect.andThen |
Effect.tapErrorCause | Effect.tapCause |
Layer.scoped | Layer.effect (он сам даёт и закрывает Scope) |
Option.fromNullable | Option.fromNullishOr |
Schema.decodeUnknown, ошибка ParseError | Schema.decodeUnknownEffect, ошибка SchemaError |
Schema.Literal('a', 'b') | Schema.Literals(['a', 'b']) |
Schema.Union(a, b) | Schema.Union([a, b]) |
FiberRef | Context.Reference |
Effect.makeSemaphore | Semaphore.make |
Stream.async | Stream.callback |
effect/TestClock | effect/testing |
@effect/platform, @effect/rpc, @effect/cluster | effect/http, effect/persistence и другие, effect/rpc, effect/cluster |
Таблица короткая нарочно. Полная карта имён в репозитории Effect занимает больше мегабайта, и ссылка на неё стоит в материалах урока. Читать её подряд не нужно: она нужна, когда встретил имя, которого нет в таблице, и хочешь узнать, куда оно ушло.
Две строки таблицы меняют не только имя. Effect.Service в v3 сам собирал слой Default и подключал зависимости; в v4 этого нет, слой ты пишешь явно, как делал с урока 47. А аксессоры убрали не из вредности: на дженерик-методе вроде get<T>(key) они теряли параметр типа, и T превращался в unknown.
Где тут дыра
Ошибка не пропала, она переехала. Компилятор молчит, потому что as any выключил ему глаза. Молчит и Effect Language Service, о котором ниже: правило про устаревшие имена смотрит на типы, а типа у any нет. Падает программа, и падает плохо. Аксессора на классе нет, вызов бросает TypeError, и сбор платы заканчивается не ошибкой TollRefused, а дефектом, который не поймает ни один catchTag.
Так выглядит почти любой перевод «по старой статье» или «по совету модели». Модели учились на годах кода v3, и в их ответах catchAll и Context.Tag встречаются чаще, чем имена 4.1. Правило простое: если перевод не собирается, читай ошибку и ищи имя в таблице. as any не переводит код, он прячет место, где перевод не закончен.
Что поменялось внутри
Пакеты @effect/platform, @effect/rpc, @effect/cluster, @effect/cli и другие въехали в сам effect. Отдельными остались только те, что тянут за собой платформу, провайдера или чужую технологию: @effect/platform-*, @effect/sql-*, @effect/ai-*, @effect/opentelemetry, @effect/atom-* и @effect/vitest. Ты пользовался этим весь курс, не замечая: effect/http в уроке 90 и effect/ai в уроке 113 пришли из того же пакета, что и Effect.
С нестабильными модулями история такая. В бетах 4.0 они жили по адресу effect/unstable/<модуль>, и путь сам предупреждал: осторожно. В 4.0.0 сегмент unstable убрали, а предупреждение переехало в документацию: у каждого модуля и каждого экспорта есть пометка @stability. unstable значит, что в минорной версии API может сломаться; модули ai, cli, cluster, http, http-api, persistence, reactivity, rpc, sql, workflow, а также process, socket, workers и другие. Путь стал короче, а осторожности нужно столько же. Поэтому во многих уроках курса стоит врезка «Нестабильно».
Рантайм файберов в v4 переписан ради памяти и скорости, а сам effect хорошо режется сборщиком: по замеру команды Effect минимальная программа весит 7,1 КБ после минификации и gzip.
Инструменты, которые читают за тебя
Читать имена по таблице полезно один раз. Дальше это делают инструменты.
Effect Language Service (@effect/language-service) это плагин TypeScript. Ставишь пакет и добавляешь его в tsconfig.json:
{
"compilerOptions": {
"plugins": [{ "name": "@effect/language-service" }]
}
}
Редактор должен работать на TypeScript из проекта, а не на встроенном, иначе плагин не загрузится. После этого редактор начинает видеть то, чего не видит обычный TypeScript:
outdatedApi: имя из v3 в проекте на v4. НаEffect.catchAllон прямо пишет, что это API v3;floatingEffect: эффект создан и брошен, никто его не запустит;missingStarInYieldEffectGen:yieldбез звёздочки вEffect.gen;missingEffectContextиmissingLayerContext: сервис, которого нет в требованиях;catchUnfailableEffect: ловушка ошибок на эффекте, который не может упасть.
Часть правил выключена по умолчанию и включается в опциях плагина. Есть и рефакторинги: превратить async-функцию в Effect.gen, собрать слои по зависимостям, сделать из интерфейса класс Schema. Наведи курсор на yield*, и подсказка покажет все три параметра эффекта.
Обычный tsc плагинов не загружает, поэтому в CI правила молчат. Есть два выхода. effect-language-service diagnostics --project tsconfig.json печатает те же предупреждения отдельной командой. effect-language-service patch правит установленный TypeScript так, что tsc сам выдаёт диагностики Effect; команду кладут в prepare в package.json, чтобы правка переживала переустановку. Ещё одна команда, layerinfo, показывает, какой слой какие сервисы даёт.
@effect/tsgo это тот же набор правил для TypeScript 7, компилятора, переписанного на Go. Он умеет и то, что раньше делали линтеры: effect-tsgo oxlint --type-aware запускает Oxlint с правилами Effect, а effect-tsgo tsc запускает пропатченный компилятор, не трогая node_modules. Проверки идут прямо при сборке типов, и типы считаются один раз.
Нестабильно. Инструменты движутся быстрее библиотеки. Урок написан на
@effect/language-service@0.87.5и@effect/tsgo@0.63.0; второй требует TypeScript 7. Имена правил и команд сверяй с README своей версии. Модули с пометкой@stability unstableизeffect@4.1.0могут сломаться в минорной версии, стабильное ядро держит semver.
Разминка
Переведи записку Гилта целиком: рабочий отчёт по башням на Effect 3 должен собраться и работать на 4.1. Тесты смотрят на поведение отчёта и сводки и на их типы, а не на то, какими именами ты пользовался.
Практика
Глубже в архиве
Как линтер находит баги, которые пропускает компилятор, и как встроить его в CI, разобрано в уроке про качество кода курса по автоматизации тестирования.
Дальше
К вечеру записка Гилта собирается на 4.1, тесты зелёные, а в бумагах конторы на одну загадку меньше. Рыдкулл снимает мантию с манекена и вешает её на спинку твоего стула.