Skip to content

TypeScript 2.0 change in IterableResult<T> makes implementing iterators harder #11375

Description

TypeScript Version: 2.0.3
The definition in lib.es6.d.ts of IteratorResult (removing the ? after value) seems to have broken existing Iterator implementations, and is particularly troublesome with strict null checking enabled. See http://stackoverflow.com/questions/39813087/why-did-typescript-2-0-change-iteratorresultk for more details.

Code
A short version:

export interface JavaIterator<E> {
    hasNext(): boolean;
    next(): E;
    remove(): void;
}
...
class IterableAdapter<T> implements Iterable<T>, IterableIterator<T> {
    private _iterator: JavaIterator<T>
    constructor(private collection: JavaCollection<T>){}

    [Symbol.iterator]() { this._iterator = this.collection.iterator(); return this;}

    next(): IteratorResult<T> {
        if (!this._iterator.hasNext()) return { done: true }; // Error on this line
        return {done: false, value: this._iterator.next()}
    }
}

this version works

class IterableAdapter<T> implements Iterable<T>, IterableIterator<T> {
    private _iterator: JavaIterator<T>
    constructor(private collection: JavaCollection<T>){}

    [Symbol.iterator]() { this._iterator = this.collection.iterator(); return this;}

    next(): IteratorResult<T> {
        if (!this._iterator.hasNext()) {
            // A bit of a hack needed here needed for strict null checking
            return { done: true, value: undefined } as any as IteratorResult<T>;
        }
        return {done: false, value: this._iterator.next()}
    }
}

original bug

class HashMapKeyIterable<K,V> implements Iterator<K>, IterableIterator<K> {
    private _bucket: HashMapEntry<K,V>[];
    private _index: number;

    constructor( private _buckets : Iterator<HashMapEntry<K,V>[]> ){
        this._bucket = undefined;
        this._index = undefined;
    }

    [Symbol.iterator]() { return this }

    next():  IteratorResult<K> {
        while (true) {
            if (this._bucket) {
                const i = this._index++;
                if (i < this._bucket.length) {
                    let item = this._bucket[i];
                    return {done: false, value: item.key}
                }
            }
            this._index = 0
            let x = this._buckets.next();
            if (x.done) return {done: true}; // Under TS 2.0 this needs to
            this._bucket = x.value;          // return {done: true: value: undefined};
            }
        }
    }

Expected behavior:
Clean compile under TypeScript 2.0

Actual behavior:
Errors reported: TS2322: Type '{ done: true; }' is not assignable to type 'IteratorResult'. Property 'value' is missing in type '{ done: true; }'

Note: with strict null checking on, I seem to need to go further, resorting to

return { done: true, value: undefined } as any as IteratorResult<T>;`

Activity

  1. yortus commented on Oct 5, 2016

    @yortus
    Contributor

    This does seem to be a bug and a divergence from IteratorResult in the ECMAScript spec. The spec states the following about the value property:

    If done is true, this is the return value of the iterator, if it supplied one. If the iterator does not have a return value, value is undefined. In that case, the value property may be absent from the conforming object if it does not inherit an explicit value property.

  2. aluanhaddad commented on Oct 5, 2016

    @aluanhaddad
    Contributor

    There is a long chain of issues around this. It is a little confusing. See #8357 for some the motivation.

  3. yortus commented on Oct 5, 2016

    @yortus
    Contributor

    Both #8357 and #8938 mention that with boolean literal types, IteratorResult can be properly fixed.

    We now have boolean literal types, but it seems it's still difficult to fix - see #2983 (comment).

    TL;DR the fix for this is now held up on default generic type variables (#2175). Unless of course someone can come up with a better alternative.

  4. BurtHarris commented on Oct 5, 2016

    @BurtHarris
    Author

    To me the fix seems easy: switch the definition in lib.es6.d.ts back to what it was in 1.8:

    interface IteratorResult<T> {
        done: boolean;
        value?: T;
    }
    

    Entangling this with an enhancement that tries to enforce complex conditions for the property's optional status seems overreaching. I don't see why it should be held up on default generic type variables.

  5. yortus commented on Oct 5, 2016

    @yortus
    Contributor

    Burt Harris (@BurtHarris):

  6. Igorbek commented on Oct 5, 2016

    @Igorbek
    Contributor

    TL;DR the fix for this is now held up on default generic type variables (#2175). Unless of course someone can come up with a better alternative.

    Troy Gerwien (@yortus) I've just wrote an alternative #2983 (comment)

  7. BurtHarris commented on Oct 5, 2016

    @BurtHarris
    Author

    Thanks Troy Gerwien (@yortus) .

    It seems to me like it's not the previous definition of IteratorResult that is conflating yield and return types, that seems like it has to be something in tsc handling of for..of.

    If I understand the background on #8357, that would only only comes about when using strict null checking. The change in lib.es6.d.ts impacts any users who upgrade to TypeScript 2.0, even if they are not using the new features.

  8. yortus commented on Oct 5, 2016

    @yortus
    Contributor

    Burt Harris (@BurtHarris) the problem in both the current and the previous IteratorResult declarations is that value is typed as T, but really must support two different types. That's because value is the yielded value when done is false, and the returned value when done is true. These values aren't necessarily (or even usually) of the same type. For example:

    function* foo() {
        yield 1;
        yield 2;
        yield 3;
        return 'done';
    }
    
    // Looping over the next() values for foo() gives:
    // { done: false, value: 1 }                                                   
    // { done: false, value: 2 }                                                   
    // { done: false, value: 3 }                                                   
    // { done: true, value: 'done' }    

    So the type of value is number when done==false, and string when done==true. The current/previous definition can't capture that because it only has one type argument T. A union of the two types wouldn't be helpful either.

  9. BurtHarris commented on Oct 5, 2016

    @BurtHarris
    Author

    Yes, but the latest issue you raise sounds like a problem with generators, not necessarily a problem with iterators. And it certainly makes sense to me that the type associated with a generator could perhaps need to account for the difference between yield and return. However, the code I'm working toward isn't (and probably won't be) dependent on generators.

    Now I'll be the first to admit I'm not an expert on generators or iterators, and that if I were designing these features I would have specified the protocol differently. But based on my reading of the iterator protocol from Mozilla.org, I inferred that in the value returned from a next() method, both done and value are optional, suggesting only one or the other need be provided. Personally, I think that part of the protocol makes sense, though it might be hard to model completely in terms of TypeScript. Perhaps that's why you are saying its dependent default generic type variables, is that right?

    But in terms of a practicing programmer (using a released tool) rather than a language designer, I would prefer that TypeScript 2 accept the old interface until the details of proper enforcement the evolved protocol can be worked out, or until I opt-in to enforcement of a more restrictive one.

  10. added
    Needs InvestigationThis issue needs a team member to investigate its status.
    and removed
    Needs InvestigationThis issue needs a team member to investigate its status.
    on May 24, 2017
  11. winterbe commented on Aug 11, 2017

    @winterbe

    I agree with Burt Harris (@BurtHarris) that the interface IteratorResult is very confusing. The protocol clearly states that an undefined value is valid if done: true, but it's currently not covered by the interface. This scenario is no corner case. I'd say pretty much every custom iterator will run into this problem when strictNullChecks is enabled (which should be the case in every project if you ask me).

  12. added and removed
    Needs InvestigationThis issue needs a team member to investigate its status.
    on Aug 23, 2017
  13. 31 remaining items

  14. fatcerberus commented on May 31, 2019

    @fatcerberus

    Not sure if this has been suggested but could we fix this by just defining IteratorResult as a discriminated union, i.e.

    type IteratorResult<T> =
        | { done: false, value: T }
        | { done: true, value?: T };
  15. LinusU commented on Jun 1, 2019

    @LinusU
    Contributor

    Bruce Pascoe (@fatcerberus) I believe that when done is true, value should always be undefined:

    type IteratorResult<T> =
        | { done: false, value: T }
        | { done: true, value: undefined };

    edit: see my later comment

  16. fatcerberus commented on Jun 1, 2019

    @fatcerberus

    No, that’s not correct:

    function* g() {
        yield 812;
        return 1208;
    }
    
    const iter = g();
    console.log(iter.next());  // { done: false, value: 812 }
    console.log(iter.next());  // { done: true, value: 1208 }
  17. LinusU commented on Jun 2, 2019

    @LinusU
    Contributor

    Ah, of course, hahaha ☺️

    It kind of has to be two types though, the return value doesn't have to be the same as the yielded value:

    function* g() {
      yield 1
      yield 2
      return 'hello'
    }
    
    const iter = g()
    
    console.log(iter.next());  // { done: false, value: 1 }
    console.log(iter.next());  // { done: false, value: 2 }
    console.log(iter.next());  // { done: true, value: 'hello' }
    type IteratorResult<T, R> =
        | { done: false, value: T }
        | { done: true, value: R };

    relevant: #2983

  18. KSXGitHub commented on Jun 2, 2019

    @KSXGitHub
    Contributor

    It kind of has to be two types though, the return value doesn't have to be the same as the yielded value

    For this particular use case, I think union type should suffice.

    const a: IteratorResult<number | string> = iter.next()
  19. LinusU commented on Jun 2, 2019

    @LinusU
    Contributor

    For this particular use case, I think union type should suffice.

    Using a type union wouldn't accurately let me work with the iterator though:

    function* g() {
      yield 1
      yield 2
      return 'hello'
    }
    
    const iter: Iterator<number | string> = g()
    
    for (let num of iter) {
      console.log(num * 2) // <- cannot multiply string with 2
    }

    This will incorrectly tell me that num can be a string...

  20. rbuckton commented on Jun 2, 2019

    @rbuckton
    Contributor

    This should be resolved by #30790

  21. fatcerberus commented on Jun 2, 2019

    @fatcerberus

    It turns out #30790 does pretty much exactly what I suggested (plus more!). Nice. 😃

  22. knocte commented on Jul 2, 2019

    @knocte

    Why close this? #30790 is a PR, not an issue, and it hasn't been merged yet.

  23. KSXGitHub commented on Jul 2, 2019

    @KSXGitHub
    Contributor

    Agreed.

    Burt Harris (@BurtHarris) can you reopen this?

  24. DanielRosenwasser commented on Jul 8, 2019

    @DanielRosenwasser
    Member

    Should be merged now.

  25. BurtHarris commented on Jul 14, 2019

    @BurtHarris
    Author

    Thank you all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

CommittedThe team has roadmapped this issueFixedA PR has been merged for this issueSuggestionAn idea for TypeScript

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions