В прошлом уроке бланк кадра научился различать виды полей. Но у кода башни есть смысл, которого нет у строки: буква T, дефис, цифры. И у номера кадра есть смысл, которого нет у числа: он целый и больше нуля. Этот урок про две вещи: как проверить смысл при разборе и как сделать, чтобы проверку было видно в типе.
Проверка поверх вида
Любую схему можно дополнить проверками через .check(...). Для частых случаев есть готовые:
import { Schema } from 'effect';
const Code = Schema.String.check(Schema.isPattern(/^T-\d+$/, { message: 'код башни вида T-число' }));
const Id = Schema.Int.check(Schema.isGreaterThan(0, { message: 'номер кадра больше нуля' }));
Schema.isPattern сверяет строку с регулярным выражением, Schema.isGreaterThan число с границей. Их много: длина строки, диапазон, кратность, непустой массив; список на странице Filters. Второй аргумент { message } даёт тексту ошибки человеческие слова, иначе их пишет сама схема по-английски. Разбор 'Т-12' и 0:
код башни вида T-число
номер кадра больше нуля
Проверка меняет только разбор. Тип остаётся прежним: string и number.
Своя проверка
Готовой проверки на всё не хватит. Башня не шлёт кадр сама себе, и маршрут T-1 → T-1 бессмыслен. Это правило про два поля сразу, и пишется оно своей проверкой:
const Route = Schema.Tuple([Code, Code]).check(
Schema.makeFilter(([from, to]) => from !== to || 'башня не шлёт кадр сама себе'),
);
Schema.makeFilter получает значение, которое уже прошло вид, и отвечает true, если всё в порядке, или текст ошибки. Порядок важен: сначала разбор вида, потом проверки. Поэтому внутри фильтра from и to уже строки, а не unknown.
башня не шлёт кадр сама себе
Есть и Schema.refine: проверка, которая заодно сужает тип, как предикат с is из урока 06.
const Calling = Schema.String.pipe(Schema.refine((s): s is `T-${string}` => s.startsWith('T-')));
type Calling = typeof Calling.Type; // `T-${string}`
Это удобно, когда у суженного типа есть имя в TypeScript. У кода башни его нет: «T, дефис и только цифры» шаблонная строка не выразит.
Печать
Проверили при разборе, а что дальше? Значение ушло в функцию отправки, оттуда в журнал, оттуда в отчёт. По типу string никто по дороге не отличит проверенный код от строки, которую кто-то собрал руками. Нужна метка в типе, которую нельзя получить, не пройдя проверку. Её называют брендом. Ты строил такой руками в уроке 14 на unique symbol. В Schema бренд ставит одна деталь:
export const TowerCode = Schema.String.check(
Schema.isPattern(/^T-\d+$/, { message: 'код башни вида T-число' }),
).pipe(Schema.brand('TowerCode'));
export type TowerCode = typeof TowerCode.Type; // string & Brand<'TowerCode'>
В рантайме ничего не меняется: значение та же строка 'T-1'. Меняется тип. Получить TowerCode можно двумя путями, и оба проверяют:
- разобрать схемой:
Schema.decodeUnknownEffect(TowerCode)(input); - собрать конструктором:
TowerCode.make('T-1')проверяет так же, как разбор, и бросает на'Т-12'. Это для кода, который пишет сама Академия, а не для входа с провода.
Перевод с Ук
Функция отправки теперь просит код с печатью:
import { Effect, Schema } from 'effect';
const TowerCode = Schema.String.check(Schema.isPattern(/^T-\d+$/)).pipe(Schema.brand('TowerCode'));
type TowerCode = typeof TowerCode.Type;
declare const sendTo: (code: TowerCode, line: string) => Effect.Effect<void>;
export const night = sendTo('T-99', 'гроза');
tower.ts(8,29): error TS2345: Argument of type 'string' is not assignable to parameter of type 'string & Brand<"TowerCode">'.
Type 'string' is not assignable to type 'Brand<"TowerCode">'.
Снизу: string не помещается в Brand<"TowerCode">. Строкой выше: аргумент типа string не подходит к параметру string & Brand<"TowerCode">. Пересечение значит «и то и другое сразу»: строка и бренд. У литерала 'T-99' есть только первая половина. Компилятор не знает и не может знать, что T-99 выглядит правильно: проверку видно только по печати, а печать ставит разбор. Чинится не приведением as TowerCode, которое ставит печать без проверки, а разбором:
const night = Effect.flatMap(Schema.decodeUnknownEffect(TowerCode)('T-99'), (code) => sendTo(code, 'гроза'));
Кадр с печатью
Бланк из прошлого урока остаётся как есть: старый договор не переписывают. Рядом встаёт кадр, которому можно верить:
export const FrameId = Schema.Int.check(
Schema.isGreaterThan(0, { message: 'номер кадра больше нуля' }),
).pipe(Schema.brand('FrameId'));
export const Frame = Schema.Struct({
...RawFrame.fields,
id: FrameId,
route: Route,
relay: Schema.optionalKey(TowerCode),
});
RawFrame.fields это объект полей схемы. Разворачиваем его и подменяем три поля: остальные четыре переписывать незачем. У Frame теперь две стороны с разными типами. typeof Frame.Type хранит route как readonly [TowerCode, TowerCode], а typeof Frame.Encoded, то, что лежит на проводе, всё ещё readonly [string, string]. Печать ставится при разборе и снимается при записи; на провод уходит обычная строка. Подробно о двух сторонах в следующем уроке.
Где тут дыра
const isTowerCode = (code: string): boolean => /^T-\d+$/.test(code);
const sendTo = (code: string, line: string) => console.log(`> ${code}: ${line}`);
const handle = (to: string) => {
if (isTowerCode(to)) sendTo(to, 'гроза');
};
handle('T-99');
const config = { neighbour: 'Т-12' };
sendTo(config.neighbour, 'утренняя сводка');
> T-99: гроза
> Т-12: утренняя сводка
В обработчике проверка есть, и T-99 прошёл честно. А утренняя сводка пошла в обход: код соседа взяли из настроек, и sendTo его принял, потому что ей годится любая строка. Русская Т ушла на провод. Проверка, которая живёт в одном if, защищает одну дорогу. Печать защищает все: с sendTo(code: TowerCode, ...) вторая строка просто не соберётся, и код из настроек придётся сначала разобрать.
Магистраль, этап 3
Кадр получает печати: код башни, номер кадра и маршрут, где башня не шлёт сама себе. Тесты проверяют и разбор, и типы: строка без печати там, где ждут TowerCode, не должна собираться.
Практика
Глубже в архиве
Бренды на голом TypeScript и зачем они на границах программы разобраны в уроке «Надёжность и типы на границах».
Дальше
Казимир переворачивает кадр Магистрали и показывает на поле, которого в бланке пока нет.
Две стороны схемы ждут в следующем уроке.