type MyMap = {
a: () => void
b: (s: string) => void
c: (n: number) => void
}
type MyKey = keyof MyMap
function wrapper(fn: MyMap[MyKey]) {
return async function(...arg:Parameters<MyMap[MyKey]>){
await Promise.resolve();
fn.apply(null, arg);
}
}
It should work because I know I pass a fn
to wrapper
, and fn.apply
the same arg
. But Typescript throws an error:
The 'this' context of type '(() => void) | ((s: string) => void) | ((n: number) => void)' is not assignable to method's 'this' of type '(this: null) => void'.
Type '(s: string) => void' is not assignable to type '(this: null) => void'.
Target signature provides too few arguments. Expected 1 or more, but got 0.
2
Answers
I don’t know that it’s possible to solve this in the way that you want but hopefully @jcalz can swoop in and correct me.
As to why your current implementation doesn’t work,
keyof MyMap
is actually the union of all possible keys inMyMap
, i.e.'a' | 'b' | 'c'
. Inside yourwrapper
function you are essentially sayingarg
is therefore inferred to be[] | [s: string] | [n: number]
asParameters
distributes acrossMyMap['a']
,MyMap['b']
andMyMap['c']
You can see why by the time you’re trying to apply
arg
tofn
, TypeScript doesn’t know the type offn
and how it relates to the type of your...arg
so it complainsIdeally what you want is to tell TypeScript that you are only accessing 1 function on the
MyMap
type so the downstream typing will work in the context of a single functionYou may be tempted to try something like this
but you will encounter the same issue because we are still extending
keyof MyMap
which is the union of all keys.Even though in practice we would use it as follows
from inside the function TypeScript still has to protect for the use case where
K
is not just a union of a single type (e.g.'a' | 'b'
, which is a perfectly validkeyof MyMap
)As far as I know, there is currently no way to inform typescript to restrict a generic expecting a union type to a single member
Instead of dealing with
MyMap
directly from withinwrapper
, you could instead extract your function before passing it intowrapper
and just infer the arguments from there. This guarantees that you will only ever be passing a single function of a fixed signature intowrapper
which TypeScript can easily deal withI imagine this goes against the essence of what you are trying to achieve with a "wrapper function", but it hopefully sheds some light on where the difficulties arise
Playground
TypeScript can’t follow the sort of correlation between the union types of
MyMap[keyof MyMap]
(a union of three function types) and theParameters<MyMap[keyof MyMap]>
(a union of three tuple types). Your intention is that when the former is(s: string) => void
then the latter will be[string]
, but the compiler is worried about possible mismatch. The general issue about the lack of language support for correlated union types is described in microsoft/TypeScript#30581.The recommended approach to such cases is to refactor to a simple "base type" and then perform generic indexed accesses into that type, or into mapped types over that type. This is described in detail in microsoft/TypeScript#47109.
For your example, that would look like this:
The base type is
MyMapArgs
, which directly represents the parameter tuples. And nowMyMap
is a mapped type overMyMapArgs
. Your function then needs to be made generic inK extends keyof MyMap
, and you can then talk aboutMyMapArgs[K]
(a simple indexed access type) directly instead ofParameters<MyMap[K]>
(using theParameters<T>
utility type which is implemented as a more complicated conditional type that the compiler can’t reason much about):That compiles just fine.
Unfortunately that you will immediately run into the fact that when you call the function, the compiler cannot narrow the type of
K
to the appropriate member (there is no value of typeK
sent to the function), and therefore the return type is inaccurate:Probably the best way to deal with this is to explicitly make some use of
"a"
,"b"
, and"c"
:Now when you call it the compiler narrows as expected:
That argument is unneeded at runtime, but it really helps the compiler understand what you’re doing. If you want to avoid that then the compiler will not follow your logic and the best you can do is use type assertions or the like, since functions at runtime do not carry information about their expected argument types.
Playground link to code