I've implemented a basic messaging system in TypeScript using implicit any
s but now I'm struggling to properly type it so that no type information is lost.
These messages are simple objects containing data used by handler functions, with a message.type
property determining which handler function to call.
There's a base interface called Message
that only includes the type
property, along with more specific interfaces that extend from it.
I'm having trouble figuring out the correct way to type this, as the compiler is throwing an error:
Type '(message: MessageA) => void' is not assignable to type 'MessageHandler'.
Types of parameters 'message' and 'message' are incompatible.
Type 'T' is not assignable to type 'MessageA'.
Property 'x' is missing in type 'Message' but required in type 'MessageA'.
Here's a simplified version of the code that reproduces the issue:
export enum MessageType {
MessageTypeA,
MessageTypeB,
}
export interface Message {
readonly type: MessageType
}
export interface MessageA extends Message {
readonly type: MessageType.MessageTypeA
readonly x: string
}
export interface MessageHandler {
<T extends Message>(message: T): void
}
const onMessageA: MessageHandler = (message: MessageA) => {
console.log(message.x)
}
While other parts of the system exist, they aren't directly related to this issue.
Given how the system operates, TS needs to infer the generic type. Declaring MessageHandler
like below won't work:
export interface MessageHandler<T extends Message> {
(message: T): void
}
I tested this code using TypeScript versions 3.8.3
and 3.9.2
.
Here's a link to play around with this code in the TypeScript Playground: link.
I also attempted declaring MessageHandler
differently, but encountered the same error:
export type MessageHandler = <T extends Message>(message: T) => void
How can I properly type MessageHandler
to accept any message as long as it has a type
property, without explicitly specifying the type during the function call?
EDIT
To provide context, I use the MessageHandler
like this:
const defaultFallback = <T extends Message>(message: T) => console.warn('Received message with no handler', message)
export type MessageHandlers = {
readonly [P in MessageType]?: MessageHandler;
}
export const makeHandler = (functions: MessageHandlers, fallback: MessageHandler = defaultFallback) => (message: Message) => {
if (!message)
return
const handler = functions[message.type]
if (handler)
handler(message)
else if (fallback)
fallback(message)
}
const onMessageA: MessageHandler = (message: MessageA) => {
console.log(message.x)
}
const onMessageB: MessageHandler = (message: MessageB) => {
...
}
makeHandler({
[MessageType.MessageA]: onMessageA,
[MessageType.MessageB]: onMessageB,
})