До сих пор провод и башня говорили на одном языке. Число из JSON было числом в башне, строка строкой; бренд из прошлого урока добавил только метку в типе. Время так не переносится. JSON не знает типа «время», и Магистраль пишет его строкой "2026-11-01T06:00:00Z". Башне нужно значение, которое можно сравнить, сдвинуть на минуту и показать в часовом поясе: DateTime из урока 74.
Две стороны
У каждой схемы два типа, и ты их уже видел:
typeof S.Type: значение в башне, после разбора;typeof S.Encoded: то, что лежит на проводе, до разбора.
У Schema.String и Schema.Number стороны совпадают. У схемы времени нет:
import { DateTime, Schema } from 'effect';
const Stamp = Schema.Struct({ id: Schema.Number, sentAt: Schema.DateTimeUtcFromString });
const stamp = Schema.decodeUnknownSync(Stamp)({ id: 7, sentAt: '2026-11-01T06:00:00Z' });
console.log(stamp, DateTime.formatIso(stamp.sentAt));
console.log(Schema.encodeSync(Stamp)(stamp));
{ id: 7, sentAt: DateTime.Utc(2026-11-01T06:00:00.000Z) } 2026-11-01T06:00:00.000Z
{ id: 7, sentAt: '2026-11-01T06:00:00.000Z' }
Разбор идёт с провода в башню: строка стала DateTime.Utc. Запись идёт обратно: DateTime.Utc снова строка. decodeUnknownSync и encodeSync это те же разбор и запись, что в уроке 76, только они бросают, а не отдают квитанцию; в примерах так короче, в башне бери версии с Effect.
Схему, у которой стороны разные, называют кодеком: она и кодирует, и раскодирует.
Перевод с Ук
Ты собираешь ответ для провода и кладёшь в него разобранную отметку:
import { Schema } from 'effect';
const Stamp = Schema.Struct({ id: Schema.Number, sentAt: Schema.DateTimeUtcFromString });
declare const stamp: typeof Stamp.Type;
export const wire: typeof Stamp.Encoded = stamp;
frame.ts(7,14): error TS2322: Type '{ readonly id: number; readonly sentAt: Utc; }' is not assignable to type '{ readonly id: number; readonly sentAt: string; }'.
Types of property 'sentAt' are incompatible.
Type 'Utc' is not assignable to type 'string'.
Снизу: Utc не строка. Выше: это поле sentAt. Ещё выше видны обе стороны целиком: слева Type с sentAt: Utc, справа Encoded с sentAt: string. Компилятор держит их раздельно, и это главная польза: значение из башни нельзя по ошибке отправить на провод без записи. Чинится не приведением, а записью: Schema.encodeSync(Stamp)(stamp) переведёт Utc в строку.
Строка JSON
Магистраль присылает не объект, а строку JSON. Её разбор тоже перевод: из строки в объект. В Schema он есть готовый:
const Wire = Schema.fromJsonString(Stamp);
console.log(Schema.decodeUnknownSync(Wire)('{"id":7,"sentAt":"2026-11-01T06:00:00Z"}'));
console.log(Schema.encodeSync(Wire)(stamp));
{ id: 7, sentAt: DateTime.Utc(2026-11-01T06:00:00.000Z) }
{"id":7,"sentAt":"2026-11-01T06:00:00.000Z"}
fromJsonString(Stamp) это два перевода подряд: строка в JSON-объект, объект в Stamp. На проводе одна строка, в башне DateTime. И ошибки приходят от обоих шагов, каждая со своим местом:
Expected a valid JSON string
Expected a valid UTC DateTime string
at ["sentAt"]
Первая на обрезанной строке {"id": 7, вторая на "sentAt":"утром". JSON.parse руками больше не нужен, и его SyntaxError не вылетит посреди башни.
Свой перевод
Готовых переводов много: числа из строк, время из строк и миллисекунд, URL, Duration. Но у башни есть формат, которого Schema не знает: её собственный провод. Маршрут на нём пишется T-9->T-1, а в кадре это кортеж из двух кодов. Перевод собирают из двух деталей:
import { Effect, Schema, SchemaIssue, SchemaTransformation } from 'effect';
const Route = Schema.Tuple([Schema.String, Schema.String]);
const ROUTE = /^(T-\d+)->(T-\d+)$/;
export const RouteFromString = Schema.String.pipe(
Schema.decodeTo(
Route,
SchemaTransformation.transformEffect({
decode: (line: string) => {
const [, from, to] = ROUTE.exec(line) ?? [];
return from === undefined || to === undefined
? Effect.fail(new SchemaIssue.InvalidValue({ message: 'не маршрут' }, line))
: Effect.succeed([from, to] as const);
},
encode: ([from, to]) => Effect.succeed(`${from}->${to}`),
}),
),
);
Читаем снаружи внутрь. Schema.String.pipe(Schema.decodeTo(Route, перевод)): на проводе строка, в башне Route. Перевод это пара функций decode и encode между закодированной стороной Route и строкой. Чужая строка это не исключение, а беда разбора SchemaIssue.InvalidValue с текстом; Schema сама поставит ей путь и завернёт в SchemaError.
[ 'T-9', 'T-1' ] T-1->T-2
не маршрут
Перевод только меняет форму. Проверять смысл, коды и «башня не шлёт сама себе», будет схема на другой стороне: если вместо Route поставить кортеж с брендами, его проверки отработают после перевода. Когда перевод не может провалиться, вместо transformEffect берут SchemaTransformation.transform с обычными функциями.
Под капотом
Любая схема это значение с полем ast, деревом SchemaAST. Узел дерева описывает сторону башни: Objects для структуры, String, Number, Declaration для DateTime.Utc. У узла, стороны которого различаются, есть цепочка encoding: шаги перевода, и у каждого шага своя цель (схема, к которой он ведёт) и пара функций.
console.log(Schema.DateTimeUtcFromString.ast._tag, Schema.DateTimeUtcFromString.ast.encoding?.map((link) => link.to._tag));
Declaration [ 'String' ]
Узел Declaration, время, и один шаг до String. Запись идёт по цепочке от узла к проводу, разбор по той же цепочке обратно, от провода к узлу. Поэтому одна схема переводит в обе стороны, и поэтому decodeTo пишется со стороны провода: он дописывает шаг в цепочку. Есть и Schema.flip(S), которая меняет стороны местами: её разбор это запись S. Стороны по отдельности, без переводов, дают Schema.toType(S) и Schema.toEncoded(S).
Где тут дыра
const Naive = Schema.Struct({ id: Schema.Number, sentAt: Schema.Date });
const line = '{"id":7,"sentAt":"2026-11-01T06:00:00Z"}';
console.log(Effect.runSync(Effect.flip(Schema.decodeUnknownEffect(Schema.fromJsonString(Naive))(line))).message);
Expected a valid Date
at ["sentAt"]
Время в строке правильное, а разбор говорит, что ждал правильную дату. Ринцвальд час ищет опечатку в строке. Дело не в строке. Schema.Date это схема значения Date: обе её стороны Date, объект JavaScript. Она годится, когда дата уже объект, например внутри башни. На проводе JSON, а JSON переносит дату только строкой. Нужен перевод: Schema.DateFromString даст Date, Schema.DateTimeUtcFromString даст DateTime.Utc. Правило: смотри, что лежит на проводе, и выбирай схему, у которой такая сторона Encoded.
Магистраль, этап 4
Кадр учится двум языкам: время отправки на проводе строкой, в башне DateTime.Utc; весь кадр Магистрали одной строкой JSON; и старый провод самой башни, маршрут|номер|тег|данные, как перевод в тот же кадр и обратно. Все проверки кадра работают после перевода, и README называет, что теряется при записи на старый провод.
Практика
Дальше
Рыдкулл заходит в мастерскую с пачкой разобранных кадров.
Классы схем ждут в следующем уроке.