Types for function that applys name of function and arguments

The compiler will not be able to understand that this is type safe because it generally does not reason very well about assignability for types that depend on as-yet-unspecified generic type parameters. There is an existing GitHub issue, microsoft/TypeScript#24085, that describes this situation.

In fact, it is possible (but not very likely) that in your function, K might be inferred as Keys itself instead of either "sum" or "concat". If you do this:

const oops = apply(Math.random() < 0.5 ? "sum" : "concat", "a", "b", "c"); // oopsie
console.log(oops); // 50% chance of "abc", 50% chance of "ab"

then you see that the compiler is technically correct that what you’re doing isn’t type safe. You’d like to tell the compiler that K will be exactly one of the members of Keys, and you can’t. See microsoft/TypeScript#27808 for a feature suggestion that would allow this.

Anyway, the compiler can’t view the funKey parameter and the args rest parameter as having correlated types. And even if it could, it’s not great at maintaining the correlation, see microsoft/TypeScript#30581 for more about that.

It also can’t understand compute the return type, so you’ll have to annotate it. You can use the ReturnType<F> utility type for this. Note that there’s also a Parameters<F> utility type which you can use instead of writing Args<F> yourself.


So when it comes down to it, you will just have to tell the compiler that what you are doing is type safe (you won’t call apply() on some union-typed funKey, right?), because it can’t verify it. And to do that you need something like a type assertion. The easiest one to use here is good old any:

type Funs = typeof funs;

function apply<K extends Keys>(funKey: K, ...args: Parameters<Funs[K]>): ReturnType<Funs[K]> {
    return (funs[funKey] as any)(...args);
}

This will allow you to do crazy things like return (funs[funKey] as any)(true), so you should be careful. Slightly more type safe but considerably more complicated is to represent funs[funKey] as a function which somehow accepts either the arguments expected by each function, and which returns both of the return types. Like this:

type WidenFunc<T> = ((x: T) => void) extends ((x: (...args: infer A) => infer R) => any) ?
    (...args: A) => R : never;

function apply<K extends Keys>(funKey: K, ...args: Parameters<Funs[K]>): ReturnType<Funs[K]> {
    return (funs[funKey] as WidenFunc<Funs[Keys]>)(...args);
}

Here WidenFunc<Funs[Keys]> is (...args: [number, number] | [string, string, string]) => number & string. That’s kind of a nonsense function type, but at least it will complain if you pass it an argument like (true) instead of (...args).


Anyway, either of those should work:

const test1 = apply('sum', 1, 2) // number
const test2 = apply('concat', 'str1', 'str2', 'str3') // string

Okay, hope that helps; good luck!

Playground link to code

Leave a Comment