Typescript: Each member of the union type has signatures, but none of those signatures are compatible with each other

Created on 25 Sep 2019  ·  8Comments  ·  Source: microsoft/TypeScript


TypeScript Version: 3.6.3


Search Terms:
array.map, expression is not callable, union type signatures,

Code

_The following code is the exact same code as in this codesandbox_
https://codesandbox.io/s/lingering-sun-7itgj

interface Product_result {
  __typename: 'Product'
  id: number
  description: string | null
}
interface Campaign_result {
  __typename: 'Campaign'
  id: number
  products: (Product_result | null)[] | null
}


interface Product {
  id: number
  description: string
  specific: boolean
}
interface Campaign {
  products: Product[]
}

type CompoundType = Campaign_result | Campaign

/* --- */

const props: { campaign?: Campaign_result } = {}
const emptyCampaign: Campaign = {
  products: []
}

const initialData: CompoundType = props.campaign || emptyCampaign


/*
Cannot invoke an expression whose type lacks a call signature. Type '
    (<U>(callbackfn: (value: Product_result, index: number, array: Product_result[]) => U, thisArg?: any) => U[])
  | (<U>(callbackfn: (value: Product, index: number, array: Product[]) => U, thisArg?: any) => U[])
' has no compatible call signatures.

product inside map is any, but I know `id` is there
*/
initialData.products.map(product => product.id)


// product is { description: string, id: number } - as expected
const product = initialData.products[0] 

// product inside map is { description: string, id: number } - as expected
;(initialData.products as Array<CompoundType['products'][0]>).map(product => product.id)


/* interestingly */

type T01 = Array<CompoundType['products'][0]>   // T01: (Product_result | Product)[]  -  I can actually have mix of both. Products that were fetched and Products added via a Form
type T02 = CompoundType['products']             // T02: Product_result[] | Product[]

declare const v1: T01
v1.map(product => product) // OK

declare const v2: T02
v2.map(product => product) // NOK

Expected behavior:

I know id is there, so this should work
initialData.products.map(product => product.id)

This workaround works
(initialData.products as Array<CompoundType['products'][0]>).map(product => product.id)

Actual behavior:

The map gives a non compatible call signature error (as in the snippet above), and the product inside is any.

Related
TypeScript 3.3 - Improved behavior for calling union types
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#caveats

Design Limitation

Most helpful comment

Hi, I also have the same problem as @Fleuv here with typescript 3.9.7. Can I ask what is the status of this issue and why it is closed ?

All 8 comments

I think this is being tracked here #7294.

We don't have any way to check this in a way that's well-bounded in terms of analysis time.

Consider if you had written something like this:

const arr1: string[] | number[] | boolean[] = [];
const arr2: string[] | number[] | boolean[] = [];
const arr3: string[] | number[] | boolean[] = [];

declare function fn(a: string, b: number, c: boolean): void;
declare function fn(a: number, b: number, c: string): void;
declare function fn(a: string, b: boolean, c: boolean): void;
declare function fn(a: number, b: number, c: string): void;

arr1.forEach(a => {
    arr2.forEach(b => {
        arr3.forEach(c => {
            // arr: ????
            const arr = [a, b, c] as const;
            fn(arr[0], arr[1], arr[2]);
        });
    });
});

The only way to correctly check this program is to re-check the body of the inner body 3 * 3 * 3 times (!), and TS would need to clear its cache of expression types on each invocation (something which is currently architecturally impossible). It's not even clear what we'd show for arr's type here!

A typical suggestion is some sort of merging of argument types into unions, but this only works in some cases (it's OK for map but doesn't make sense for filter, for example).

I see, that's understandable. So, is there an official best-practice™ workaround for this or should I resort to manually extract the types like this?

type T01 = Array<CompoundType['products'][0]>

I have solved in that way.

type CompoundType = (Campaign_result | Campaign)&{products:  (Product|Product_result)[]}
const arr1: string[] | number[] | boolean[] = [];
const arr2: string[] | number[] | boolean[] = [];
const arr3: string[] | number[] | boolean[] = [];

declare function fn(a: string, b: number, c: boolean): void;
declare function fn(a: number, b: number, c: string): void;
declare function fn(a: string, b: boolean, c: boolean): void;
declare function fn(a: number, b: number, c: string): void;

arr1.forEach(a => {
    arr2.forEach(b => {
        arr3.forEach(c => {
            // arr: ????
            const arr = [a, b, c] as const;
            fn(arr[0], arr[1], arr[2]);
        });
    });
});

It's not even clear what we'd show for arr's type here!

I'm probably missing something here, but to me arr would be [string | number | boolean, string | number | boolean, string | number | boolean]. This would fail to compile because the first parameter of fn doesn't accept a boolean value.

I think https://github.com/microsoft/TypeScript/pull/29011 solved exactly that and https://github.com/microsoft/TypeScript/pull/31023 should solve remain issues with function overloads (such as .map).

I also experienced this issue with the following use case.

TypeScript Version: 3.9.2

Code

interface Base {
    id: number;
}

interface Book extends Base {
    title: string;
    author: Author;
}

interface Author extends Base {
    name: string;
}

interface Models {
    books: Book[];
    authors: Author[];
}

const author1: Author = {
    id: 1,
    name: 'Foo Bar',
};

const data : Models = {
    books: [
        {
            id: 1,
            title: "hello world",
            author: author1,
        }
    ],
    authors: [
        author1,
    ],
};

// This does work
function getBookById(id: number): Book | undefined {
    return data.books.filter((item: Base) => item.id = id)[0];
}

// This is more generic but does not work
/**
 * Error message:
 * 
 * This expression is not callable. Each member of the union type '
 * { 
 *      <S extends Book>
 *          (callbackfn: (value: Book, index: number, array: Book[]) => value is S, thisArg?: any): S[];
 *          (callbackfn: (value: Book, index: number, array: Book[]) => unknown, thisArg?: any): Book[];
 * } | { ...; }
 * ' has signatures, but none of those signatures are compatible with each other.
 */
function getById<T extends Base>(type: keyof Models, id: number): T | undefined {
    return data[type].filter((item: Base) => item.id = id)[0];
}

Playground Link

And here's another really simple illustration:

interface A {
    a: number;
}
interface B {
    b: string;
}

const c: A[] | (A | B)[] = [];

c.map(z => z) // <- Error
c.forEach(z => z) // <- OK

Note that forEach works here, so with a closure & a little refactor, you can make this work in practice

Hi, I also have the same problem as @Fleuv here with typescript 3.9.7. Can I ask what is the status of this issue and why it is closed ?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

uber5001 picture uber5001  ·  3Comments

bgrieder picture bgrieder  ·  3Comments

fwanicka picture fwanicka  ·  3Comments

DanielRosenwasser picture DanielRosenwasser  ·  3Comments

kyasbal-1994 picture kyasbal-1994  ·  3Comments