Non
Non

Reputation: 8589

TS error: Cannot invoke an expression whose type lacks a call signature

I am using lodash on a TypeScript app and I am getting an error

Cannot invoke an expression whose type lacks a call signature. Type '((callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any) => U[]) | ((callbackfn: (value: Phone, index: number, array: Phone[]) => U, thisArg?: any) => U[])' has no compatible call signatures.ts(2349)

Function:

        {get(userInfo, 'phone', []).map((item, index) => (
          <View style={styles.userPhone} key={index}>
            {item.type === 'Desk' && <Text>Phone: {item.num}</Text>}
            {item.type === 'Mobile' && <Text>Mobile: {item.num}</Text>}
          </View>
        ))}

If I do this it works correctly:

        {userInfo.phone &&
           userInfo.phone.map((item, index) => (
            <View style={styles.userPhone} key={index}>
              {item.type === 'Desk' && <Text>Phone: {item.num}</Text>}
              {item.type === 'Mobile' && <Text>Mobile: {item.num}</Text>}
            </View>
        ))}

So what could I be missing?

PS: Lodash get docs: https://lodash.com/docs/#get

I have installed types package for lodash -> https://www.npmjs.com/package/@types/lodash

The thing is that I can not do it like this {userInfo.phone && userInfo.phone.map(...)} because we are enforced to use get for this type of things.

Upvotes: 0

Views: 10482

Answers (1)

jcalz
jcalz

Reputation: 328758

Since I don't have lodash typings available, I'll just assume that the get() function behaves more or less as follows:

type ReplaceUndefined<T, V> = T extends undefined ? V : T;

declare function get<T, K extends keyof T, V>(
  obj: T,
  key: K,
  dflt: V
): ReplaceUndefined<T[K], V>;

This means that if key of type K is a key of obj of type T, then get() will return a value of type T[K]... unless that value may be undefined, in which case it will replace undefined with the type of the dflt value, like this:

const val = get({ a: Math.random() < 0.5 ? "a" : undefined }, "a", 123); // string | number

Also, I'm asssuming the following type and value declarations:

interface Phone {
  type: string;
  num: string;
}
declare const userInfo: { phone?: Phone[] };

Now I'm ready to start answering the question. When I try to call get() like this, we get an error:

get(userInfo, "phone", []).map((item, index) => "Number: " + item.num); // error!
// "cannot invoke an expression that lacks a call signature"

The problem is that the default value [] is inferred by the compiler to be of type never[]. This the return value of get() will be the union type Phone[] | never[], and thus the map method of this union will itself be a union of the method types for Phone[] and for never[]. It is a longstanding issue in TypeScript that you usually can't call unions of functions. This has been improved in recent language versions, but there are still limitations, such as when more than one function in the union is generic. And since map() is generic, TypeScript doesn't know how to call it on Phone[] | never[].


The obvious fix here is that you don't want [] to be treated as never[]; instead you'd like it to be treated as an empty Phone[]. If you can do that, then what comes out of get() will be Phone[] | Phone[], which is just Phone[], and therefore its map() method is no longer a union type, and is callable.

Here are two ways to do it... one is with a type assertion:

get(userInfo, "phone", [] as Phone[]).map((item, index) => "Number: " + item.num); // okay

and the other is with a type annotation on a new variable:

const noPhones: Phone[] = [];
get(userInfo, "phone", noPhones).map((item, index) => "Number: " + item.num); // okay

Okay, hope that helps. Good luck!

Link to code

Upvotes: 5

Related Questions