Type Signature for a method, that proxies call to a collection of actions

Viewed 37

In our library, we have a collection of predefined actions, that can be run.

Instead of exposing these actions as part of the API, a single method shall be exposed to call the action, as we need to run some other independent actions aside the action itself (e.g. log the executed actions). But I'm currently stuck in the implementation of the actual action call. Below is the best I could do currently. Typescript is able to correctly suggest the arguments based on the passed handlerName to callHandler, but it fails at the spread parameter, complaining, that the argument is not a tuple. But the Parameters type returns a tuple, if I understand it correctly.

Please see the code below or in the typescript playground. Thanks for any hints in advance!

/**
 * We have a predefined set of functions, with a fixed call signature.
 * This is predefined in the Handlers type:
 */
type Handlers = {
    add: {
        handler: (argument: string) => string;
    };
    count: {
        handler: (argument: number) => string;
    }
};

const myHandlers : Handlers = {
    add: {
        handler: (argument: string) => `Add ${argument}`,
    },
    count: {
        handler: (argument: number) => `I count to ${argument}`,
    }
};

/**
 * Now, we want to be able, to define a method, that proxies the calls to these functions (to do other checks upfront).
 * How can this be typed safely?
 */
function callHandler<H extends keyof Handlers>(name: H, ...params: Parameters<Handlers[H]["handler"]>) {
    return myHandlers[name].handler(...params);
}


// Example call:
console.log(callHandler("count", 10));
1 Answers

I was able to make it work like this, first the type declaration as you had it before with some addons ;) (notice you can add more argument types inside the array like this [number, string, ...] and then you can disrupt also in the implementations or just grab it one by one as handler: (arg_0: type0, arg_1: type1...)=>{}):

type Handlers = {
     add: {
        handler: (...args: [string]) => string;
     };
     count: {
        handler: (...args: [number]) => string;
     };
};

now you need to create the constraint to make sure you are not accessing a property that doesn't exist in the object (in this case you need to make sure first that add or count exist inside the Type Handlers). Like the docs suggested.

export function callHandler<Type, Key extends keyof Type>(obj: Type, key: Key) {
    return obj[key];
}

this guarantees that you can only use the keys inside the object you pass inside the callHandler function in the first parameter obj, now to call the function you do it like this:

const myHandlers: Handlers = {
    add: {
        handler: (argument: string) => `Add ${argument}`,
    },
    count: {
        handler: (argument: number) => `I count to ${argument}`,
    },
};
console.log(callHandler(myHandlers, 'count').handler(10)); //I count to 10
console.log(callHandler(myHandlers, 'add').handler("something")); //Add something
console.log(callHandler(myHandlers, 'count').handler("something")); //Error: Argument of type 'string' is not assignable to parameter of type 'number'.
console.log(callHandler(myHandlers, 'bbbb').handler("10")); //Argument of type '"bbbb"' is not assignable to parameter of type 'keyof Handlers'.

Hope this helps! let me know if you have any questions!

Related