Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Iterator of array intersection incorrectly typed #57002

Closed
rotu opened this issue Jan 10, 2024 · 7 comments
Closed

Iterator of array intersection incorrectly typed #57002

rotu opened this issue Jan 10, 2024 · 7 comments
Labels
Duplicate An existing issue was already created

Comments

@rotu
Copy link

rotu commented Jan 10, 2024

🔎 Search Terms

array intersection iterator

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about intersection

⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.4.0-dev.20240109&ssl=22&ssc=1&pln=1&pc=1#code/C4TwDgpgBAglC8UDeUCGAuKBnYAnAlgHYDmUAvgNoC6AUKJFAEILJQBGmhArgLZsS5y1OuGgBhFnABkTGjQDGAe0I40uTBMQUUGKACIAZosV6ANO0wBGAEwBmcrREMAGizEVufAbQD0PqAEAegD8ckaCABRKKsBQAB5QigZqAJTINAF+AdlBoQHRWIoANhAAdEWKxBFxpagpGVAFxWUVVTVs9WRyBbH4sYiouBQAyiB8xaV9AqjAirhUEfXhUFHKqglJUH1pSA1ZObkNTSXlldW19flrzadtpR1QWQAKuIqQuKBQAORsX1AAJooIFgoIRFLEIHF8KplFB6NAvjpMDgCCQANzkL40LpAA

💻 Code

type A = { a: string }[]
type B = { b: number }[]
type C = A & B

const ar: C = [{ a: "foo", b: 123 }]

type X = C[number]
//   ^?

for (const x of ar) {
  //       ^?
  console.log(x.a)
  console.log(x.b)
}

const it = ar[Symbol.iterator]()
for (const x of it) {
  //       ^?
  console.log(x.a)
  console.log(x.b) // Property 'b' does not exist on type '{ a: string; }'
}

🙁 Actual behavior

Using the iterator protocol (Symbol.iterator) to iterate over an intersection array A[] & B[]

🙂 Expected behavior

I expect the iterator returned by Symbol.iterator to have the same element type as the for...of syntax sugar, i.e. (A[number] & B[number])

Additional information about the issue

No response

@rotu
Copy link
Author

rotu commented Jan 10, 2024

Same cause as #41874, though slightly more egregious example. Perhaps someArray[Symbol.iterator]/Array.prototype[Symbol.iterator] should return the type of this[number] when the function is invoked instead of binding the generic when the property is accessed?

@jcalz
Copy link
Contributor

jcalz commented Jan 10, 2024

Duplicate of #39693 more or less?

@rotu
Copy link
Author

rotu commented Jan 10, 2024

@jcalz As written, #39693 is now fixed. The issue here is something you call out in that issue - namely that the typing for Symbol.iterator is different from the element type!

@craigphicks
Copy link

craigphicks commented Jan 13, 2024

So this is bogus:

const iter: (() => IterableIterator<{
    a: string;
}>) & (() => IterableIterator<{
    b: number;
}>)

The result of getPropertyOfType(A&B,[Symbol.iterator] should be

IterableIterator<{
    a: string;
} & {
    b: number;
}>

I suppose it should be able to figure out from first principles without knowing a priori what an iterator has to be,
but because the rhs of [Symbol.iterator] is constrained that makes it a little easier and a short cut can be taken.
Can't actually depend upon the read values being IterableIterator, just something with the same shape, although the return can be IterableIterator without problem.

@rotu
Copy link
Author

rotu commented Jan 13, 2024

@craigphicks I don’t understand what you’re trying to say.

the result of getPropertyOfType(A&B,[Symbol.iterator] should be

I don’t know if you’re commenting on property access on an intersection type, the return type of calling an intersection of callables, and/or the intersection of two instantiations of IterableIntersection<T>.

I think something like this is the correct solution: #41451.

I also think the intersection of callable types should be order-independent, but that’s likely too breaking of a change :-)

@craigphicks
Copy link

craigphicks commented Jan 13, 2024

@craigphicks I don’t understand what you’re trying to say.

I was talking about # 1. below. But I have expanded the analysis to include # 2. # 2 is far more important.

Two approaches:

1. Treating [Symbol.iterator] as a special case

I was wrong to say "bogus" - it should work, even if it is a complex structure.

What I am saying is that anything on the rhs of a [Symbol.iterator] has a shape dictated by ECMA specs.
So it is possible, in the case of union or intersection of properties, to hard code a shortcut that reaches into the structures to get their iterator return type (for the done false case) and generate what is effectively ()=>IterableIterator<...>. (Uh, what about the case when a non-iterable iterator is present?).

The members of the union/intersection can be unrelated types (not both arrays, for example), it doesn't matter, that would still work because of the ECMA standard for [Symbol.iterator].

One justification for doing so is speedup.

41451 is interesting, but what I am talking about is much lower level.

2. Consider the general case

Running the TypeScript test harness shows:

const it = ar[Symbol.iterator]()
>it : IterableIterator<{ a: string; }>
>ar[Symbol.iterator]() : IterableIterator<{ a: string; }>
>ar[Symbol.iterator] : (() => IterableIterator<{ a: string; }>) & (() => IterableIterator<{ b: number; }>)

So could it be that in the general case the return type of an intersection of two functions

()=>R1 && ()=>R2

is resolved to be only the return type of first function? It seems so:

declare const f: (()=>string) & (()=>number);
const r = f(); // string

It think that in this case the result should be the intersection union (*) of all return types. *(*To be precise, here & provides the lower bound, | provides the upper bound. Generally TypeScipt deals in the upper bound).

Compare and contrast these two cases:

  1. An intersection of two functions. Note an intersection has no inherent order:
declare const f: (()=>string) & (()=>number);
const rf = f(); // string, but we really want string | number
  1. An overload, i.e. a single function with ordered, multiple signatures:
declare const g: {
  ():string;
  ():number;
}
const rg = f(); // string, that's what we want in the current framework (single-match)

They both get routed to chooseOverload and treated as a single function with multiple signatures, where just one of those signatures is selected.

This is a case where intersection and overload should not be treated same, at least in the current framework.

Interestingly, proposal #57004 would return A flow-or B in both cases because chooseOverload(s) would make multiple matches. So that would be a solution to this particular case, at least. Yeah, it is not A&B, but A flow-or B is the actual upper-bound type returned in flow analysis space, which is a real thing. It could be post-massaged to make fit the TypeScript spec A&B, when the intersection was a & type and not a real overload.

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Jan 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants