The following code will not compile in TypeScript 2.1.4 giving the error:
Error
Error:(6, 31) TS2349:Cannot invoke an expression whose type lacks a call signature. Type '((args: string[], action: A) => string[]) | ((args: string[], action: C) => string[])' has no compatible call signatures.
Code
/*
* Set up a function to take some arguments
* and an action and use the map to run the appropriate
* function bases on the type property of the action
*/
const caller = (args: string[] = [], action): string[] => {
return map[action.type] ? map[action.type](args, action) : args;
};
interface Action {
type: any;
}
const TYPE_A = "type_a";
interface A extends Action {
from: number;
to: number;
id?: number; // optional parameters causing the issue.
prop1?: number;
}
const TYPE_B = "type_b";
interface B extends Action {
from: number;
to: number;
}
const TYPE_C = "type_c";
interface C extends Action {
id: number;
prop1: number;
}
const map = {
[TYPE_A]: (args: string[], action: A) => {
return ["a"];
},
[TYPE_B]: (args: string[], action: B) => {
return ["b"];
},
[TYPE_C]: (args: string[], action: C) => {
return ["c"];
}
};
caller([], {type: TYPE_A, from: 2, to: 1});
Motivation
My motivation for using an expression as the property in the map is so that I can change the value of the property constants without needing to refactor the map.
Solutions
There are two ways of solving this:
a) Remove the optional fields in interface A
.
interface A extends Action {
from: number;
to: number;
id: number; // optional parameters causing the issue not optional.
prop1: number;
}
b) Change the map properties declarations to values and not expressions and keep optional fields.
const map = {
"type_a" : (args: string[], action: A) => {
return ["a"];
},
"type_b": (args: string[], action: B) => {
return ["b"];
},
"type_c": (args: string[], action: C) => {
return ["c"];
}
};
Question
My question is why is the error shown in the first place, can someone explain this to me?
The reason is that
A
andC
are incompatible becauseprop1
is optional inA
and required inC
. So you can't use function that takesC
in place where a function that takesA
is needed:errors:
When you declare a map with literal property names, type inference can figure out when you do
caller([], {type: TYPE_A, from: 2, to: 1});
, you are actually accessing a value with"type_a"
key, so it knows that function parameter type is exactlyA
. It can't do that when map is declared with calculated keys, probably because it just does not evaluate expressions for keys at compile time, so it infers a union type for map values, and two members of the union are incompatible with each other becauseA
andC
are incompatible.You can also get around this by just explicitly declaring type for
map
:also works.