1
votes

This code works:

class A {}
class B {}
class C {}

const classCFromAOrB = (element: A | B): C => new C()

const a: A[] | B[] = [new A()]

const c: C[] = a.map(element => classCFromAOrB(element))

This code doesn't:

import { classA } from '../some'
import { classB } from './../some'

interface interfaceC {}

const render = (element: classA | classB): interfaceC => {
    return {}
}

interface ResultsScreenProps {
    resultsScreenPresented: boolean
    answers: classA[] | classB[]
    dismiss: SimpleFunc
}

const Screen: React.SFC<ResultsScreenProps> = (props) => {
    const array: classA[] | classB[] = props.answers
    const second: interfaceC[] = array.map(el => render(el)) // here is the error
    ...
}

On the line defining second I'm getting an error:

[ts] Cannot invoke an expression whose type lacks a call signature. Type '((callbackfn: (value: classA, index: number, array: classA[]) =>...' has no compatible call signatures.

What am I doing wrong?

The error is reproducible if classA looks like this:

class classA {
    anyArg: number

    constructor(anyArg: number) {
        this.anyArg = anyArg
    }
}
1
What is ResultsScreenProps?Explosion Pills
@ExplosionPills, added declarationVladyslav Zavalykhatko
What is SimpleFunc?Explosion Pills
By the way I don't get this error; you might have transcribed something incorrectly. Could you post the actual code that's causing the issue?Explosion Pills
You can't call methods which are union types. Either redefine the type from classA[] | classB[] to (classA | classB)[], or widen the value to that type before you call map on it.jcalz

1 Answers

9
votes

As I mentioned in the comments, you can't call methods which are union types. The call signature of (classA[] | classB[])['map'] is

(
  <U>(
    callbackfn: (value: classA, index: number, array: classA[]) => U,
    thisArg?: any
  ) => U[]
) | (
  <U>(
    callbackfn: (value: classB, index: number, array: classB[]) => U,
    thisArg?: any
  ) => U[]
)

And the compiler gives up. What you can do is widen your type from (classA[] | classB[]) to (classA | classB)[]. The former is "either this is an array of all classA elements, or it's an array of all classB elements", while the latter is "this is an array of elements, each of which is either a classA or a classB". The former is more specific, since if you know arr[0] is a classA, then arr[1] will also be a classA... whereas the latter is less specific, since arr[0] might be a classA while arr[1] might be a classB. One thing good about the latter is that (classA | classB)[]['map'] has the single signature:

<U>(
  callbackfn: (value: classA | classB, index: number, array: (classA | classB)[]) => U, 
  thisArg?: any
) => U[]

and you can call that.


Your next question, "why does it stop working if I define anything in any of the classes" has to do with structural typing. In short, TypeScript thinks classA and classB are the same type if they have the same members. This might be surprising since many other typed languages use nominal typing, where two types with different names are necessarily different types. But TypeScript doesn't really work that way.

If classA and classB are both empty of properties, they will be seen as equivalent to {}, the empty type. And (classA[])|(classB[]) then reduces to ({}[])|({}[]) which is just ({}[]). And that isn't a union, so you can call its map method.

If you want classA to be seen as different from classB by the compiler, then you should give them different properties, at least until (and unless) TypeScript ever gets more first-class nominal typing.


Hope that helps you. Good luck.