How to map over a List of Discriminate Unions in the presence of overloaded functions?

Viewed 64

Given the following code:

type Foo = {
    foo: number
}

type Bar = {
    bar: string
}

type FooOrBar = 
    | Foo
    | Bar

type FooOrBarList =
    | Foo[]
    | Bar[]

function processFooOrBar(x: Foo): Foo;
function processFooOrBar(x: Bar): Bar;
function processFooOrBar(x: FooOrBar): FooOrBar {
    return x
}

function processFooOrBarList(x: Foo[]): Foo[];
function processFooOrBarList(x: Bar[]): Bar[];
function processFooOrBarList(x: FooOrBarList): FooOrBarList {
    return x.map(processFooOrBar)
}

I get the following error on x.map(processFooOrBar)

This expression is not callable. Each member of the union type ' (<U>(callbackfn: (value: Foo, index: number, array: Foo[]) => U, thisArg?: any) => U[]) | (<U>(callbackfn: (value: Bar, index: number, array: Bar[]) => U, thisArg?: any) => U[])' has signatures, but none of those signatures are compatible with each other.ts(2349)

Playground link

I do understand what the error message tells me but I dont get why.

Is this a compiler bug (because the compiler should be able to unify those) or am I running against some hard limits concerning type theory?

1 Answers

There are 3 problems with the code above.

  • The first is that Array.map is generic and can not call union types properly.
    The Problem is described here (https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#improved-behavior-for-calling-union-types) and a possible solution here (Typescript: How to map over union array type?)
    So the soution in the case above looks like this

    function processFooOrBarList(x: FooOrBarList): FooOrBarList {
        return (x as Array<Foo | Bar>).map(processFooOrBar)
    }
    
  • However this is not enough as the inferred return type does not match the defined return type so we have to cast again.

    function processFooOrBarList(x: FooOrBarList): FooOrBarList {
        return (x as Array<Foo | Bar>).map(processFooOrBar) as FooBarList
    }
    
  • There is still a problem now due to the presence of the overloaded processFooOrBar functions and the inability of the compiler to select the correct overload. So we need to give the compiler another hint via a cast.

    function processFooOrBarList(x: FooOrBarList): FooOrBarList {
        const f = processFooOrBar as (x: FooOrBar) => FooOrBar  
        return (x as Array<Foo | Bar>).map(y => f(y)) as FooOrBarList
    }
    

We do this by explictly selecting the correct function signature for the function f to be called by Array.map

  • Finally I have set all params to readonly to prevent errors by changing the underlying array which due to the casting will not be found.

A full working example can be found here Playground link

Related