В прошлом уроке записка была из четырёх полей, все числа и строки. Настоящий кадр Магистрали устроен сложнее: в нём есть слова из короткого списка, пары, словари, сообщения разных видов и поле, которого может не быть. Для каждого случая в Schema есть своя деталь, и из них, как из кубиков, собирается бланк.
Кадр Магистрали
{
"id": 7,
"route": ["T-9", "T-1"],
"priority": "urgent",
"headers": { "смена": "ночь" },
"message": { "kind": "text", "body": "гроза" },
"relay": "T-5"
}
Шесть полей и седьмое, relay, только у кадров, которые шли через ретранслятор. Разберём их по одному.
Слово из списка
Срочность бывает low, normal или urgent. Это не любая строка, а одно из трёх слов, и схема так и пишется:
export const Priority = Schema.Literals(['low', 'normal', 'urgent']);
export type Priority = typeof Priority.Type; // 'low' | 'normal' | 'urgent'
Schema.Literals берёт список значений, а тип выводит объединением литералов, как в уроке 05. Одно значение пишут Schema.Literal('text'). Кадр со срочностью high разбор не пропустит:
Expected "low" | "normal" | "urgent"
at ["priority"]
Один из четырёх
Сообщение кадра бывает четырёх видов, и поле kind говорит, какого:
export const Text = Schema.Struct({ kind: Schema.Literal('text'), body: Schema.String });
export const Ack = Schema.Struct({ kind: Schema.Literal('ack'), of: Schema.Int });
export const Ping = Schema.Struct({ kind: Schema.Literal('ping') });
export const Lost = Schema.Struct({ kind: Schema.Literal('lost'), since: Schema.Int });
export const Message = Schema.Union([Text, Ack, Ping, Lost]);
export type Message = typeof Message.Type;
Schema.Union берёт массив схем, и значение подходит, если подходит хоть к одной. Выведенный тип это размеченное объединение из урока 07, и switch по kind сужает его так же, как сужал сообщения шифра:
const describe = (message: Message): string => {
switch (message.kind) {
case 'text':
return `текст: ${message.body}`;
case 'ack':
return `принято ${message.of}`;
case 'ping':
return 'пинг';
case 'lost':
return `потеряно с ${message.since}`;
}
};
Номера в ack и lost это Schema.Int: целое число. Дробь 1.5 номером кадра не бывает, а NaN JSON вообще не перенесёт. Сообщение незнакомого вида разбор отвергает и перечисляет, что ждал:
Expected { readonly "kind": "text", ... } | { readonly "kind": "ack", ... } | { readonly "kind": "ping", ... } | { readonly "kind": "lost", ... }
at ["message"]
Пара и словарь
Маршрут это ровно две башни. Массив любой длины тут не годится, нужен кортеж:
const Route = Schema.Tuple([Schema.String, Schema.String]); // readonly [string, string]
Служебные строки Магистрали это словарь: имена заранее неизвестны, но и имя, и значение строки.
export const Headers = Schema.Record(Schema.String, Schema.String); // { readonly [x: string]: string }
Маршрут из одной башни и служебная строка с числом:
Missing key
at ["route"][1]
Expected string
at ["headers"]["смена"]
Путь в ошибке ведёт прямо к месту: второй элемент маршрута, служебная строка смена.
Поле, которого может не быть
relay есть только у кадров через ретранслятор. В схеме для этого две детали, и разница между ними тонкая:
Schema.optionalKey(Schema.String): ключа может не быть, но если он есть, в нём строка. Тип поляreadonly relay?: string.Schema.optional(Schema.String): ключа может не быть, а если он есть, в нём строка илиundefined. Типreadonly relay?: string | undefined.
Под флагом exactOptionalPropertyTypes, который стоит у шага Т, это разные типы, и это не придирка. «Ключа нет» и «ключ есть, а в нём undefined» для JavaScript разные объекты: 'relay' in frame отвечает на них по-разному. Для кадра Магистрали правильный ответ optionalKey: ретранслятор либо был и у него есть код, либо его не было.
Собираем бланк:
export const RawFrame = Schema.Struct({
id: Schema.Number,
route: Schema.Tuple([Schema.String, Schema.String]),
priority: Priority,
headers: Headers,
message: Message,
relay: Schema.optionalKey(Schema.String),
});
export type RawFrame = typeof RawFrame.Type;
Перевод с Ук
Ты собираешь кадр в башне и берёшь ретранслятор из строки маршрута:
import { Schema } from 'effect';
const RawFrame = Schema.Struct({
id: Schema.Number,
relay: Schema.optionalKey(Schema.String),
});
type RawFrame = typeof RawFrame.Type;
declare const relayOf: (line: string) => string | undefined;
export const frame: RawFrame = { id: 7, relay: relayOf('T-9->T-1') };
frame.ts(11,14): error TS2375: Type '{ id: number; relay: string | undefined; }' is not assignable to type '{ readonly id: number; readonly relay?: string; }' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
Types of property 'relay' are incompatible.
Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
Снизу вверх. undefined нельзя положить туда, где ждут string. Строкой выше: это поле relay. Ещё выше главное: у цели relay?: string, ключа может не быть, но undefined в нём не бывает, и всё это под флагом exactOptionalPropertyTypes. Совет компилятора «добавь undefined в тип цели» здесь как раз неправильный: тогда бланк станет optional, и в кадре появится ключ без значения. Правильно не класть ключ, когда ретранслятора нет:
const relay = relayOf('T-9->T-1');
export const frame: RawFrame = relay === undefined ? { id: 7 } : { id: 7, relay };
Где тут дыра
const Loose = Schema.Struct({
id: Schema.Number,
relay: Schema.optional(Schema.String),
});
const relayOf = (line: string): string | undefined => line.split('->')[2];
const hops = (frame: object): number => ('relay' in frame ? 2 : 1);
const frame = Schema.decodeUnknownSync(Loose)({ id: 7, relay: relayOf('T-9->T-1') });
console.log(frame, hops(frame));
{ id: 7, relay: undefined } 2
Кадр шёл напрямую, а башня насчитала два перегона: ключ relay есть, хоть в нём и пусто. optional пропустил undefined и сохранил ключ. Со схемой optionalKey разбор ответил бы Expected string at ["relay"], и кадр без ретранслятора пришлось бы собрать без ключа. Шире не значит надёжнее: бланк, который принимает больше, чем бывает в мире, пропускает ошибки дальше.
Магистраль, этап 2
Академия пишет полный бланк кадра Магистрали: слово срочности, четыре вида сообщения, маршрут, служебные строки и ретранслятор. Коды башен пока простые строки; печать на них поставим в следующем уроке.
Практика
Глубже в архиве
Размеченные объединения как способ описать предметную область разобраны в уроке «Алгебраические типы и моделирование домена».
Дальше
Ринцвальд смотрит в бланк через плечо и хмыкает.
Печати на значениях ждут в следующем уроке.