I have a type MyMap with 2 different keys that take an array of callbacks with different signatures.
type MyMap = {
type1: (() => void)[]
type2: ((data: string) => void)[]
}
I want to create a generic function that takes a key and a callback, and add it to an object of type MyMap.
const map: MyMap = { type1: [], type2: [] };
function addToMap<T extends keyof MyMap>(k: T, cb: MyMap[T][number]): void {
map[k].push(cb);
}
TypeScript shows me the error when i try to push the callback in the array:
Argument of type '(() => void) | ((data: string) => void)' is not assignable to parameter of type '() => void'.
Type '(data: string) => void' is not assignable to type '() => void'.
Target signature provides too few arguments. Expected 1 or more, but got 0.(2345)
Is there a way to make TypeScript understand that the callback I provided is the right one for the key?
TypeScript can't "see" the fact that every member of MyMap
with key K
is an array that allows you to push
a value of type MyMap[K][number]
onto it. It can verify the for an individual K
, but when K
is generic, it doesn't know how. It lacks the ability to automatically convert a list of facts into a single generality. So it doesn't see map[k].push
as a single coherent method; instead it sees it as a union of methods, and any correlation between map[k]
and cb
is lost.
If you want the compiler to follow your logic, you'll need to explicitly represent the operations in terms of such a generality. The recommended approach is described in microsoft/TypeScript#47109. Starting from your MyMap
type,
type MyMap = {
type1: (() => void)[]
type2: ((data: string) => void)[]
}
const map: MyMap = { type1: [], type2: [] };
we can create the "base" version of the type, corresponding to just the elements of the arrays:
type MyMapBase = { [K in keyof MyMap]: MyMap[K][number] }
/* type MyMapBase = {
type1: () => void;
type2: (data: string) => void;
} */
And then we can write addToMap()
as a generic function operating on MyMapBase
explicitly:
function addToMap<K extends keyof MyMap>(k: K, cb: MyMapBase[K]): void {
const m: { [K in keyof MyMap]: MyMapBase[K][] } = map;
m[k].push(cb);
}
Here, we've just assigned map
to a variable m
of the mapped type {[K in keyof MyMap]: MyMapBase[K][]}
. The compiler is happy with the assignment because it has to verify each property individually, and it works. But now m
is written explicitly as a type that abstracts over MyMapBase
generically. So m[k]
is of the single generic type MyMapBase[K][]
. And as such, you can easily push()
a value of type MyMapBase[K]
onto it.
Note that this would be a lot more straightforward looking if we just started with MyMapBase
in the first place, since that's the actual underlying type that your abstracting over. But if you already have MyMap
defined somewhere, the code here shows you can still compute the required things from it.