Typescript unionstype av funksjoner blir merkelig nok en snittstype av argumentene
Jeg prøver å modellere dataene mine i Typescript på følgende måte. Hovedfokus er typen MessageHandler som kobler type i en melding med en callback som aksepterer den meldingen. Jeg fulgte håndboken kapittel for kartlagte typer
type BaseMessageBody = {
msg_id?: number;
in_reply_to?: number;
};
type BaseMessage<TBody> = {
src: string;
dest: string;
body: BaseMessageBody & TBody;
};
type BroadcastMessage = BaseMessage<{
type: 'broadcast';
message: number;
}>;
type InitMessage = BaseMessage<{
type: 'init';
node_id: string;
node_ids: string[];
}>;
type Message = BroadcastMessage | InitMessage;
type Body = Message['body'];
type MessageHandler = {
[M in Message as M['body']['type']]?: (message: M) => void;
};
Så langt så bra. Den utvidede typen av MessageHandler er
type MessageHandler = {
broadcast?: ((message: BroadcastMessage) => void) | undefined;
init?: ((message: InitMessage) => void) | undefined;
}
Men når jeg prøver å faktisk bruke typen som nedenfor:
const handlers: MessageHandler = {};
export const handle = (message: Message) => {
if (message.body.type === 'init') {
console.log('Initialized');
}
const handler = handlers[message.body.type];
if (!handler) {
console.warn('Unable to handle type', message.body.type);
return;
}
handler(message); //Error here
};
Jeg får følgende feil. På en eller annen måte har handler-typen blitt transformert til const handler: (message: BroadcastMessage & InitMessage) => void
error: TS2345 [ERROR]: Argument of type 'Message' is not assignable to parameter of type 'BroadcastMessage & InitMessage'.
Type 'BroadcastMessage' is not assignable to type 'BroadcastMessage & InitMessage'.
Type 'BroadcastMessage' is not assignable to type 'InitMessage'.
Types of property 'body' are incompatible.
Type 'BaseMessageBody & { type: "broadcast"; message: number; }' is not assignable to type 'BaseMessageBody & { type: "init"; node_id: string; node_ids: string[]; }'.
Type 'BaseMessageBody & { type: "broadcast"; message: number; }' is missing the following properties from type '{ type: "init"; node_id: string; node_ids: string[]; }': node_id, node_ids
handler(message);
~~~~~~~
Det finnes noenlunde relaterte spørsmål på Stack Overflow, men jeg klarte ikke å løse problemet mitt ved å følge dem. Her er lekeplassen med all koden.
Svar #1
TypeScript har ikke direkte støtte for det jeg kaller «korrelerte unioner», som beskrevet i microsoft/TypeScript#30581. Kompilatoren kan bare typekontrollere en kodeblokk som handler(message) én gang. Hvis typen til handler er en unionstype av funksjoner som ((message: BroadcastMessage) => void) | ((message: InitMessage) => void) og typen til message er en union av argumenter som BroadcastMessage | InitMessage, kan ikke kompilatoren se at det er trygt. Tross alt, for vilkårlige handler og message-variabler av disse typene, kunne det være en feil å tillate kallet; hvis message var av typen InitMessage men handler var av typen (message: BroadcastMessage) => void, ville du hatt et problem. Den eneste sikre måten å kalle en union av funksjoner på, er med et snitt av argumentene, ikke en union av argumentene. Et unionsargument kunne vise seg å være feil type for parameteren til union-funksjonen.
I ditt tilfelle er det selvfølgelig umulig for den feilen å oppstå, fordi typen til handler og typen til message er korrelert på grunn av at de kommer fra samme kilde. Men den eneste måten å se det på ville være om kompilatoren kunne analysere handler(message) én gang for hver mulig innsnevring av message. Men det gjør den ikke. Så da får du den feilen.
Hvis du bare vil undertrykke feilen og gå videre, kan du bruke en typepåstand:
handler(message as BroadcastMessage & InitMessage); // 🤷♂️
Det er teknisk sett en løgn, men det er enkelt. Men det betyr ikke at kompilatoren ser på det du gjør som typesikkert; hvis du ved et uhell skrev noe som handler(broadcastMessageOnly as BroadcastMessage & InitMessage) (forutsatt at broadcastMessageOnly er av typen BroadcastMessage og ikke InitMessage), ville ikke kompilatoren fanget feilen. Men det spiller kanskje ingen rolle, så lenge du er sikker på at du har implementert det riktig.
Hvis du bryr deg om at kompilatoren skal verifisere typesikkerhet her, er den anbefalte fremgangsmåten for å håndtere korrelerte unions å refaktorere bort fra unions og mot generiske indekser i enkle nøkkel-verdi objekt-typer eller avbildede typer over slike objekt-typer. Denne teknikken er beskrevet i detalj i microsoft/TypeScript#47109. For ditt eksempel ser de relevante endringene slik ut:
Først la oss lage den grunnleggende nøkkel-verdi-typen vi skal bygge ut fra:
interface MessageMap {
broadcast: { message: number };
init: { node_id: string; node_ids: string[] };
}
Da kan du omdefinere din Message-type til å ta en bestemt nøkkel som en typeparameter:
type Message<K extends keyof MessageMap = keyof MessageMap> =
{ [P in K]: BaseMessage<MessageMap[P] & { type: P }> }[K]
Hvis du trenger dine opprinnelige meldingstyper, kan du gjenopprette dem:
type BroadcastMessage = Message<"broadcast">;
// type BroadcastMessage = { src: string; dest: string;
// body: BaseMessageBody & { message: number; } & { type: "broadcast"; };
// }
type InitMessage = Message<"init">;
// type InitMessage = { src: string; dest: string; body: BaseMessageBody &
// { node_id: string; node_ids: string[]; } & { type: "init"; };
// }
Og siden Message<K> har et standardtypeargument som tilsvarer hele unionen av nøkler, er bare Message i seg selv ekvivalent med din opprinnelige versjon:
type MessageTest = Message
// type MessageTest = { src: string; dest: string;
// body: BaseMessageBody & { message: number; } & { type: "broadcast"; };
// } | { src: string; dest: string; body: BaseMessageBody &
// { node_id: string; node_ids: string[]; } & { type: "init"; };
// }But
Og MessageHandler kan også skrives i form av MessageMap:
type MessageHandler = {
[K in keyof MessageMap]?: (message: Message<K>) => void;
};
Endelig gjør du handle til en generell funksjon som aksepterer en Message<K>, og feilen forsvinner:
export const handle = <K extends keyof MessageMap>(message: Message<K>) => {
if (message.body.type === 'init') {
console.log('Initialized');
}
const handler = handlers[message.body.type];
if (!handler) {
console.warn('Unable to handle type', message.body.type);
return;
}
handler(message); // okay
// const handler: (message: Message<K>) => void
};
Når du ringer handler, ser kompilatoren det som typen (message: Message<K>) => void. Det er ikke lenger en union av funksjoner; det er en enkelt funksjon med en parametertype på Message<K>. Og siden message sin type også er Message<K>, er anropet tillatt.
Er det verdt å refaktorere til denne formen? Hvis du er sikker på at din opprinnelige versjon fungerer og vil fortsette å fungere, er det absolutt enklere å bare bekrefte og gå videre. Hvis du er mindre sikker, kan det hende at refaktoreringen her er verdt innsatsen. Det avhenger av dine bruksområder.
Lenke til kode på lekeplassen
