TypeScript refuses Iterator in a for...of

Viewed 422

The following code works in my browser without the typings, but TypeScript emits an error saying that gen() doesn't have a [Symbol.iterator] method which is what is expected for an Iterable. This limitation seems strange to me as AFAIK an Iterator is a valid object to pass to for...of.

function *gen(): Iterator<number> {
  yield 1
  yield 2
  yield 3
}

for (const val of gen()) {
  console.log(val)
}

Could you explain me what I'm doing wrong here?

Edit: Removing the return type from the previous code thus letting TypeScript guess what it is gave me IterableIterator, which is making TypeScript happy. So am I not allowed to use a mere Iterator in a for...of?

3 Answers

You need to be targeting a recent ES version, pass the --downlevelIteration flag, and have as a minimum the "dom" and "esnext" libs for this to work.

function* gen() {
    yield 1;
    yield 2;
    yield 3;
}

for (let val of gen()) {
    console.log(val);
}

Transpiled demo:

"use strict";
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
var __values = (this && this.__values) || function (o) {
    var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0;
    if (m) return m.call(o);
    return {
        next: function () {
            if (o && i >= o.length) o = void 0;
            return { value: o && o[i++], done: !o };
        }
    };
};
var e_1, _a;
function gen() {
    return __generator(this, function (_a) {
        switch (_a.label) {
            case 0: return [4 /*yield*/, 1];
            case 1:
                _a.sent();
                return [4 /*yield*/, 2];
            case 2:
                _a.sent();
                return [4 /*yield*/, 3];
            case 3:
                _a.sent();
                return [2 /*return*/];
        }
    });
}
try {
    for (var _b = __values(gen()), _c = _b.next(); !_c.done; _c = _b.next()) {
        var val = _c.value;
        console.log(val);
    }
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
    try {
        if (_c && !_c.done && (_a = _b["return"])) _a.call(_b);
    }
    finally { if (e_1) throw e_1.error; }
}

You apparently need to use Iterable<T> instead of Iterator<T>. Iterable is the interface that contains the [Symbol.iterator] method that for-of loops invoke, which should return an Iterator whose next() method produces the series of values.

function* gen(): Iterable<number> {
  yield 1
  yield 2
  yield 3
}

TypeScript's current handling of Generators seems incomplete. It has a built-in Generator type definition, but it's not generic (the produced values are always any). It would also cause an error here because it's only marked as an Iterator, not Iterable, but we can see that the actual Generator object your browser produces is also Iterable (by returning itself, because it's also an Iterator).

const g = gen();
g[Symbol.iterator]() == g; // true

I think the answer to my problem is that I made a wrong statement, thinking that an Iterator is a valid object to feed to for...of.

The reason the example code without typings works in a browser is that the return value of a generator is both an Iterator and an Iterable, but only an Iterable is accepted for for...of, as written in the MDN page.

By giving the generator an explicit Iterator return type I was narrowing it in a way that made TypeScript think it didn't have the Iterable capability.

The following code using only an Iterator fails as expected in my browser

const it = {
  next: () => ({value: 1, done: false})
}

for (const val of it) {
  console.log(val)
}
Related