#Calling a mixture of union and intersection types

32 messages · Page 1 of 1 (latest)

chrome spear
#

Hello everyone,

I have this trivial code inspired by a case in a production codebase ()

type GNode = {
    id: number;
    // foo: number; // uncomment for problems
}

type GGNode = GNode & {
    tag: string;
}

type Graph = {
    nodes: GNode[];
}

type GGraph = Graph & {
    nodes: GGNode[];
}

//

type AGNode = {
    id: number;
}

type AGGNode = AGNode & {
    tag: string;
}

type AGraph = {
    nodes: AGNode[];
}

type AGGraph = AGraph & {
    nodes: AGGNode[];
}

const x = {} as GGraph | AGGraph

x.nodes.map(({ id }) => id)

If I uncomment the line with foo, I get this error:

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

I understand what the error is saying, but I don't understand the reason why an extra field in the underlying type causes this to fail. If someone has any pointers - a link where I can read more is totally enough - I'd be grateful. I tried googling and found a lot of pre-4.2 posts, but these, as far as I understand, do not apply anymore.

Thank you in advance!

vast pebble
#

I think you have here a rather obfuscated case of the correspondence problem

#

x.nodes has with how the language currently works the type (GNode[] & GGNode[]) | (AGNode[] & AGGNode[])

#

then accessing map on that leaves you with the type in your error message

#

but when you call that it will try to contravariantly resolve

chrome spear
#

oh I did not realize contravariance is the issue here, thank you

#

!resolved

vast pebble
#

Just a question

#

I'm not actually seeing any error when I enter this in the playground

frozen gorgeBOT
#
angryzor#0

Preview:```ts
type GNode = {
id: number
// foo: number; // uncomment for problems
}

type GGNode = GNode & {
tag: string
}

type Graph = {
nodes: GNode[]
}

type GGraph = Graph & {
nodes: GGNode[]
}

//

type AGNode = {
id: number
}

type AGGNode = AGNode
...```

chrome spear
#

you need to uncomment the line with foo

vast pebble
#

right

#

haha

chrome spear
#

idk why I posted it here like this - sorry 🙂 I shared the case with my colleagues to highlight the weirdness of the current design and then copy pasted

#

anyway I still need to read more about this because there are 2 overloads of map in case I omit foo in this example, but with foo I get the union type. I'd love to understand how the inference works

vast pebble
#

That's simply because TS typing is structural, so if you don't add the extra property it will just consider both versions the same type and everything collapses down

chrome spear
#

I was thinking about this but then, when I add something else (like foo) to GGNode it just works

#

oh!, but that will be because the actual id I am accessing is in the LHS type of the intersection? let me try

#
    id: number;
}

type GGNode = GNode & {
    tag: string;
}

type Graph = {
    nodes: GNode[];
}

type GGraph = Graph & {
    nodes: GGNode[];
}

type AGNode = {
    id: number;
}

type AGGNode = AGNode & {
    tag: string;
}

type AGraph = {
    nodes: AGNode[];
}

type AGGraph = AGraph & {
    nodes: AGGNode[];
}

const x = {} as GGraph | AGGraph

x.nodes.map(({ tag }) => tag)

hmm. interesting. I think I came to some fundamental thing I do not understand here. I thought this would work without any problems, since GGraph and AGGraph use the node types that contain the tag

vast pebble
#

Well you also have lists of GNodes in there, which do not have a tag

chrome spear
#

my gosh, this is it, I do not understand how intersection of list types works

y.map(({tag}) => tag) // not OK

const z : (GNode & GGNode) = { id: 1, tag: 'onetwo'}
z.tag // OK
#

I thought GNode[] & GGNode[] just behaves like (GNode & GGNode)[]

vast pebble
#

Yes, GNode[] & GGNode[] !== (GNode & GGNode)[]

#

Which is different behavior than for objects and function overloads which tend to collapse along nested entries.

#

And that last behavior I currently think is actually not entirely "correct" but I'm still investigating

chrome spear
#

thanks a lot this is all very enlightening

vast pebble
#

What you are dealing with is a major limitation of the language as it works today

#

And all these things are interlinked and related to each other, we refer to it as the correspondence problem:

#

!:correspondence

frozen gorgeBOT
#
retsam19#0
`!retsam19:correspondence-problem`:

There's a particular pattern that is safe but hard for the Typescript compiler to handle, which I call the "correspondence problem":

const functionsWithArguments = [
  { func: (arg: string) => {}, arg: "foo" },
  { func: (arg: number) => {}, arg: 0 },
];

for (const { func, arg } of functionsWithArguments) {
  func(arg);
//     ^^^
// Argument of type 'string | number' is not assignable to parameter of type 'never'.
//   Type 'string' is not assignable to type 'never'.
}

The problem is that func is typed as (x: string) => void | (x: number) => void and arg is string | number, but the compiler can't prove that they "correspond": that, for example, arg is only a string when func accepts strings.

As far as the type are concerned, arg could be number, and func could be (arg: string) => void, and that would be a type-error. It's easy for us to see that that won't happen, but that requires understanding the program at a higher-level than the level the compiler operates.

Depending on the specifics there's sometimes clever fixes, but usually I recommend using a type assertion and ignoring the issue:

func(arg as never);
vast pebble
#

I am personally working on trying to find a solution but it's quite complex and has tendrils all over the way unions and intersections are handled

chrome spear
#

thanks for the explanation and pointers, this poured energy and excitement into me today. it is extremely interesting, I am going to read what you sent me and then some more 🙏