microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.48k stars 12.43k forks source link

Naked generic type returned from iterator method #59161

Open reverofevil opened 3 months ago

reverofevil commented 3 months ago

πŸ”Ž Search Terms

generic generator static

πŸ•— Version & Regression Information

⏯ Playground Link

Link

πŸ’» Code

class Base {
    static *[Symbol.iterator]<T>(this: T) {
        yield this;
    }
}
class Child extends Base {}
const test = function* () { yield* Child };
const value = [...test()][0]; // T
console.log(value); // Child

πŸ™ Actual behavior

The result is typed as T, whatever generic parameter outside of generic code means.

πŸ™‚ Expected behavior

T should be instantiated with Child during a call to [Symbol.iterator]

Additional information about the issue

For regular static method calls it works as expected, by instantiating T

class Base {
    static foo<T>(this: T) {
        return this;
    }
}
class Child extends Base {}
const value = Child.foo(); // Child
console.log(value); // Child

On the other hand, this is a nice way to create unique types for opaque type implementation! πŸ™ƒ

Andarist commented 3 months ago

This is a fun one too:

class Base {
  static *[Symbol.iterator]<T extends [1, 2]>() {
    yield {} as T;
  }
}
class Child extends Base {}
const test = function* () {
  yield* Child;
};
const value = [...test()][0];
//    ^? const value: T extends [1, 2]

Basically, all type parameters leak in this situation.

reverofevil commented 3 months ago

Also it doesn't have to be static. I left it in the example, because otherwise it's harder to see that the behavior is incorrect.

This is all it actually takes:

class Base {
  *[Symbol.iterator]<T>() {
    yield {} as T;
  }
}

const x = [...new Base()][0];
//    ^? const x: T

I tried another Symbol() there, and it works fine.

Andarist commented 3 months ago

The proper fix for this is to thread the obtained [Symbol.iterator] signatures through resolveCall. Just like this PR does things with [Symbol.hasInstance]: https://github.com/microsoft/TypeScript/pull/55052

I have this working locally but it needs a lot of cleanup (implementation of iteration protocol has many layers/helper functions) and investigating all of the other ways of iterating through things. The problem is not exclusive to spreads:

class Base {
  *[Symbol.iterator]<T>() {
    yield {} as T;
  }
}

for (const element of new Base()) {
  element;
  // ^? const element: T
}
reverofevil commented 2 months ago

Oof! It actually doesn't do resolveCall stuff at all! Not only this shouldn't compile (foo comes from where?), but if you leave only one signature, it infers correct type.

declare class Base {
  [Symbol.iterator](foo: 1): Generator<3, void, undefined>;
  [Symbol.iterator](foo: 2): Generator<4, void, undefined>;
  [Symbol.iterator](foo: any): Generator<3 | 4, void, undefined>;
}

const x = [...new Base()];
//    ^? const x: never[]