cl10k
cl10k

Reputation: 951

Typescript: Refer to function argument inside the function signature

How can I refer to a specific function argument right inside the function signature?

enter image description here

The image above is a fake just for demonstration! I provide name and arg and need to retrieve some type info for arg by stuffing name into another function. But I don't know how to refer to name inside the function signature.


Context:

I'm working on a simple event manager. You can define event-names, assign callbacks to those and fire events. Callbacks may have an arbitrary number of arguments.

(The example below is very reduced, the real implementation is more complex, e.g. events can have multiple callbacks etc)

type CallbackType = (...args: any[]) => void;

const eventRegistry: Map<string, CallbackType> = new Map();

const registerEvent = (name: string, callback: CallbackType): void => {
        eventRegistry.set(name, callback);
}

const fireEvent = (name: string, ...args: any[]): void => {
    eventRegistry.get(name)!(...args);
}

some example callbacks

const foo = (input: number): void => {
    console.log("foo here:", input);
}

const bar = (input1: boolean, input2: string): void => {
    console.log("bar here:", input1, input2);
}

event registration and event firing

registerEvent("someEventName", foo);
registerEvent("anotherEventName", bar);

fireEvent("someEventName", 42);
fireEvent("anotherEventName", true, "Hello World");

Here's a link to a typescript playground.


The implementation above is not type-safe. You don't get any hints in fireEvent what arguments the callbacks expect.

So I tried using typescripts Parameters utility type:

const fireEvent = (name: string, ...args: Parameters<typeof eventRegistry.get(name)>): void => {
    eventRegistry.get(name)!(...args);
}

But that got me nowhere. To correctly type args I first need to get the callback assigned to the event but I don't know how to refer to name.

Upvotes: 0

Views: 407

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250196

You can use Parameters to get the arguments of a function, and you can use spread to spread the arguments back to a function. You can even use generic type parameters and index types to get the type of the function from an object typoe:

type EventRegistry = {
    someEventName: typeof foo
    anotherEventName: typeof bar
}

const eventRegistry: Map<string, CallbackType> = new Map();
const fireEvent = <K extends keyof EventRegistry>(name: K, ...args: Parameters<EventRegistry[K]>): void => {
    eventRegistry.get(name)!(...args);
}

fireEvent("anotherEventName", true, "") // ✅
fireEvent("anotherEventName", true, 0) // ❌

Playground Link

Your real problem here is that registerEvent does not in any way leave a mark on the type of fireEvent. This means that fireEvent does not know what events were registered, if we want this information at compile time, we need to flow it from the register calls to the fireEvent calls.

Here is my stab at it, not a definitive solution, but a starting point:

class EventRegistry<T extends  Record<string, CallbackType> = {}>{
    eventRegistry: Map<string, CallbackType> = new Map();

    registerEvent<K extends string, C extends CallbackType>(name: K, callback: C):EventRegistry<T & Record<K, C>>   {
        this.eventRegistry.set(name, callback);
        return this;
    }

    fireEvent<K extends keyof T & string>(name: K, ...args: Parameters<T[K]>): void  {
        this.eventRegistry.get(name)!(...args);
    
}
const reg = new EventRegistry()
    .registerEvent("someEventName", foo)
    .registerEvent("anotherEventName", bar);

reg.fireEvent("someEventName", 42); // ✅
reg.fireEvent("anotherEventName", true, "Hello World"); // ✅
reg.fireEvent("anotherEventName", true, 0); // ❌

Playground Link

Upvotes: 1

Related Questions