1

I want to have a function that returns a new function whose type is dependent on the parameters passed in to the wrapping function. The wrapping function should take as parameters a function and an object. Here is some of the type info:

type Event = {};

type EventExecutor<T extends Event> = (event: T) => void;

type EventMap<T extends Event> = {
    [id: string]: (...args: any) => T;
};

and the function would be implemented something like:

function createEventHandler<
    T extends Event,
    M extends EventMap<T>,
    K extends keyof M
>(executor: EventExecutor<T>, eventMap: M) {
    return (id: K, ...params: Parameters<M[K]>) => {
        const event = eventMap[id](...params);
        executor(event);
    };
}

The way I expect it to work is:

type MyEventA = { type: 'foo', fizz: string };
type MyEventB = { type: 'bar', buzz: number };

type MyEvent = MyEventA | MyEventB;

function myEventExecutor(event: MyEvent) {
    // ...
}

const myEventMap = {
    hello: (p: string) => ({ type: 'foo', fizz: p }),
    goodbye: (n: number) => ({ type: 'bar', buzz: n }),
};

const myHandler = createEventHandler(myEventExecutor, myEventMap);

myHandler('hello', 'world'); // correct
myHandler('goodbye', 42); // correct
myHandler('hello', 42); // ERROR
myHandler('goodbye', 'world'); // ERROR
myHandler('wrong', 'stuff'); // ERROR

There's some issues with what I currently have. One it seems like I lose all the type info for id in myHandler...any string passes without an error. Same goes for the parameters as well.

I'm really not sure what the issue is tbh since the type info seems like it makes sense???

Additionally, I would like to be able to have the event map be either the function that returns the generic Event OR just that generic Event (in other words a static event)... Event | (...args: any) => Event ... but I'm fine if I'm not able to do that.

2 Answers 2

1

I think the thing you're missing is that the return value of createEventHandler() should itself be a generic function, like this:

function createEventHandler<
    T extends Event,
    M extends EventMap<T>,
    K extends keyof M
>(executor: EventExecutor<T>, eventMap: M) {
    return <P extends K>(id: P, ...params: Parameters<M[P]>) => {
        const event = eventMap[id](...params);
        executor(event);
    };

}

Then, you also need to make sure that the type of your myEventMap is as narrow as possible (so that type is typed as 'foo' and not as string). If you're using TS3.4+ you can use a const assertion:

const myEventMap = {
    hello: (p: string) => ({ type: 'foo', fizz: p } as const),
    goodbye: (n: number) => ({ type: 'bar', buzz: n } as const),
};

Otherwise you could work around it a number of ways (such as {type: 'foo' as 'foo', fizz: p}).

Given those changes, you get the behavior you're looking for:

myHandler('hello', 'world'); // correct
myHandler('goodbye', 42); // correct
myHandler('hello', 42); // ERROR
myHandler('goodbye', 'world'); // ERROR
myHandler('wrong', 'stuff'); // ERROR

Hope that helps; good luck!

Sign up to request clarification or add additional context in comments.

1 Comment

I added an additional answer using yours as a starting point in order to allow static events. Thanks!
0

Extending @jcalz's answer to include the additional requirement I asked, which is that properties in EventMap can be either the function that returns the Event or just a static Event. This seems a little hacky but it seems to work well. Here's the new type information for EventMap:

type EventMap<T extends Event> = {
    [id: string]: T | ((...args: any) => T);
};

I added an additional property to myEventMap:

const myEventMap = {
    hello: (p: string) => ({ type: 'foo', fizz: p } as const),
    goodbye: (n: number) => ({ type: 'bar', buzz: n } as const),
    bonjour: { type: 'foo', fizz: 'something' } as const,
};

This required me to write a helper type to extract parameters if the type was a function:

type HandlerParams<T> = T extends ((...args: any[]) => any) ? Parameters<T> : Array<undefined>;

and so the re-implemented createEventHandler looks like:

function createEventHandler<
    T extends Event,
    M extends EventMap<T>,
    K extends keyof M
>(executor: EventExecutor<T>, eventMap: M) {
    return <P extends K>(id: P, ...params: HandlerParams<M[P]>) => {
        const eventProp = eventMap[id];
        let event: T;
        if (typeof eventProp === 'function') {
            event = (eventMap[id] as ((...args: any) => T))(...params);
        } else {
            event = eventProp as any; // couldn't get this working any other way, aka the hack
        }
        executor(event);
    };
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.