I have a very simple messaging system implemented in TypeScript using implicit anys and am trying to type it properly so no type information is lost anywhere.
These messages are simple objects which carry some information used by handler functions. All messages have a message.type property, which is used to decide which handler function gets called.
There's a base interface Message which only defines this type property and then there are specific interfaces which extend from it.
I can't quite figure out how to type this properly, and am not sure what the exact issue is.
As it is, the compiler is failing with this error message:
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 is a simplified version of the code that can reproduce 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)
}
There are other parts to the messaging system but are not directly relevant, I believe.
Due to how the rest of the system works, I need TS to infer the generic type. I can't, for example, declare MessageHandler as follows:
export interface MessageHandler<T extends Message> {
(message: T): void
}
Tried this code with TypeScript 3.8.3 and 3.9.2.
Here's a link to this code in the TypeScript Playground: link.
I also tried declaring MessageHandler as follows, yet got the same error:
export type MessageHandler = <T extends Message>(message: T) => void
How can I properly type MessageHandler so it can accept any kind of message as long as it has a type property, without needing to explicitly pass the type when called?
EDIT
Adding some context, I'm using 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,
})
onMessageAshould "accept any kind of message as long as it has atypeproperty" then its implementation is bad because it will explode for most message types. IfonMessageAshould only accept aMessageAargument, then its annotated type is bad and you need to use something like the version you say you can't use, whereMessageHandler<T>is itself generic. Why can't you use it? Can you include an example that shows why that doesn't work for you?