Search code examples
typescriptpublish-subscribe

Restrict method signature to only allow payload of type defined in event map


I am working on an event (pub/sub) system and I need to define the events and payloads upfront. Having trouble restricting the publish method to EVENT, EVENT[PAYLOAD].

// This is the closest I got to, but it isn't working:
class SomeClass<EVENT_MAP, EVENTNAME extends keyof EVENT_MAP> {
    publish(eventName: EVENTNAME, payload: EVENT_MAP[EVENTNAME]) {}
}

enum EVENTS {
    HELLO,
    WORLD
}

type EVENT_SIGNATURES = {
    [EVENTS.HELLO]: {
        messageId: string
    },
    [EVENTS.WORLD]: {
        age: number
    }
}
// This does not work as expected. What am I doing wrong?
const someClass = new SomeClass<EVENT_SIGNATURES, keyof EVENT_SIGNATURES>();

// Here's how I expect it work:
someClass.publish(EVENTS.HELLO, { messageId: 123 }) // => error. string expected
someClass.publish(EVENTS.HELLO, { messageId: "123", age: 15 }); // => error. age shouldn't be on here
someClass.publish(EVENTS.HELLO, { messageId: '123' }) // => good

someClass.publish(EVENTS.WORLD, { messageId: '123' }) // => error. messageId isn't on the type
someClass.publish(EVENTS.WORLD, { age: 123 }) // => good

// Here's what actually happens
someClass.publish(EVENTS.HELLO, { messageId: 123 }); // => error. string expected
someClass.publish(EVENTS.HELLO, { messageId: "123", age: 15 }); good
someClass.publish(EVENTS.HELLO, { messageId: "123" }); // => good

someClass.publish(EVENTS.WORLD, { messageId: "123" }); // => good
someClass.publish(EVENTS.WORLD, { age: 123 }); // => good

This is the signature that typescript infers:

(method) SomeClass<EVENT_SIGNATURES, EVENTS>.publish(eventName: EVENTS, payload: {
    messageId: string;
} | {
    age: number;
}): void

Whereas I need payload to be restricted to the EVENTNAME.

Here's a sandbox too: https://codesandbox.io/s/bold-joliot-1bi9m?file=/src/index.ts:591-693

Thanks in advance!


Solution

  • Try to make publish generic, to remember the eventName that was passed in:

    class SomeClass<EVENT_MAP> {
        publish<EVENTNAME extends keyof EVENT_MAP>(eventName: EVENTNAME, payload: EVENT_MAP[EVENTNAME]) {}
    }