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?

1

There are 1 answers

0
artem On BEST ANSWER

The reason is that A and C are incompatible because prop1 is optional in A and required in C. So you can't use function that takes C in place where a function that takes A is needed:

let fa: (a: A) => void;
let fc: (c: C) => void;

fa = fc;
fc = fa;

errors:

test.ts(49,1): error TS2322: Type '(c: C) => void' is not assignable to type '(a: A) => void'.
  Types of parameters 'c' and 'a' are incompatible.
    Type 'A' is not assignable to type 'C'.
      Property 'id' is optional in type 'A' but required in type 'C'.
test.ts(50,1): error TS2322: Type '(a: A) => void' is not assignable to type '(c: C) => void'.
  Types of parameters 'a' and 'c' are incompatible.
    Type 'C' is not assignable to type 'A'.
      Property 'from' is missing in type 'C'.

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 exactly A. 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 because A and C are incompatible.

You can also get around this by just explicitly declaring type for map:

const map: {[key: string]: (args:string[], action: Action) => string[]} = {
    [TYPE_A]: (args: string[], action: A) => {
        return ["a"];
    },
    [TYPE_B]: (args: string[], action: B) => {
        return ["b"];
    },
    [TYPE_C]: (args: string[], action: C) => {
        return ["c"];
    }
};

also works.