简体   繁体   中英

Dynamic Typescript function parameters from object properties

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 Event | (...args: any) => Event ... but I'm fine if I'm not able to do that.

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!

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);
    };
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM