Вывести объединение WebSocket-сообщений и карту обработчиков из одного реестра
WebSocket-протокол шлёт объекты, размеченные полем type. Объединение и карту обработчиков сейчас пишут руками, и они расходятся при добавлении каждого сообщения.
Требования: выведите оба из реестра Messages ниже, чтобы добавление сообщения туда было единственной правкой. Message — размеченное объединение { type: K } & Messages[K], Handlers сопоставляет каждому type обработчик ровно этого сообщения, а dispatch направляет сообщение своему обработчику, и каждый обработчик видит свой суженный payload. Никакого any, никаких as.
type Messages = {
ping: { at: number };
chat: { room: string; text: string };
};
type Message = /* ваш код здесь */;
type Handlers = /* ваш код здесь */;
function dispatch(msg: Message, handlers: Handlers): void {
// ваш код здесь
}
Допишите реализацию.
Отображённый тип, индексированный через keyof, выводит из реестра объединение { type: K } & Messages[K]; второй по тем же ключам выводит карту обработчиков, где каждый принимает своё сообщение. dispatch делает switch по разметке, и новое сообщение не скомпилируется.
- ✗Пересекать
keyofс объединением payload, сочетая каждое имя с каждой формой - ✗Писать объединение руками рядом с реестром, из-за чего они расходятся
- ✗Считать, что отображённый тип не может дать объединение объектных типов
- →Почему
dispatchстановится ошибкой компиляции сразу после добавления сообщения в реестр? - →Как расширить решение, если наборы сообщений сервера и клиента различаются?
Решение
type Messages = {
ping: { at: number };
chat: { room: string; text: string };
};
// Объединение выводится из реестра: по одному варианту на ключ
type Message = { [K in keyof Messages]: { type: K } & Messages[K] }[keyof Messages];
// = ({ type: "ping" } & { at: number }) | ({ type: "chat" } & { room: string; text: string })
type Handlers = {
[K in keyof Messages]: (msg: Extract<Message, { type: K }>) => void;
};
function dispatch(msg: Message, handlers: Handlers): void {
switch (msg.type) {
case "ping":
handlers.ping(msg); // msg сужен до { type: "ping"; at: number }
return;
case "chat":
handlers.chat(msg); // msg сужен до { type: "chat"; room: string; text: string }
return;
default: {
const never: never = msg; // новое сообщение в реестре ⇒ ошибка компиляции здесь
throw new Error(`unhandled ${JSON.stringify(never)}`);
}
}
}
Почему именно так
{ [K in keyof Messages]: ... }[keyof Messages]— отображённый тип проиндексированный своими же ключами. Индексация объединяет значения свойств, поэтому на выходе объединение вариантов, а не один объект.- Пересечение
{ type: keyof Messages } & Messages[keyof Messages]выглядит короче, но сочетает каждое имя с каждым payload —{ type: "ping"; room: string }тоже пройдёт. Разметка перестаёт что-либо различать. Extract<Message, { type: K }>вытаскивает ровно тот вариант, чейtypeравенK, поэтомуhandlers.chatне приметping.- Ветка
defaultс присваиванием вnever— проверка исчерпывающести: добавилиcloseвMessages— здесь сразу ошибка типов.