📘 TypeScript

Итераторы и генераторы в TypeScript: подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представьте, что вы — библиотекарь, и вам нужно переписать все книги с полки. У вас есть два подхода:

Первый подход (без итератора): Вы снимаете все книги сразу, складываете их огромной кучей на столе и только потом начинаете переписывать. Проблема: стол может не вместить все книги, а если книг миллион — вы просто не сможете их поднять.

Второй подход (с итератором): Вы подходите к полке, берёте первую книгу, переписываете её, ставите на место. Затем берёте вторую — и так, пока не дойдёте до последней. Вы не храните всё в памяти одновременно, а работаете с каждой книгой по очереди.

Итератор — это как раз такой «библиотечный помощник», который умеет ходить по коллекции (массиву, списку, строке) и выдавать элементы по одному. Он помнит, на каком элементе остановился, и может сказать: «Есть ещё элементы?» или «Дай следующий».

Генератор — это фабрика по производству итераторов. Если итератор — это курьер, который приносит вам пиццу по одной, то генератор — это пиццерия, которая может испечь новую пиццу по вашему запросу. Генератор сам решает, когда остановиться и что отдать следующим.

💡 Важно: Итераторы и генераторы решают одну главную задачу — ленивые вычисления. Мы не храним все данные в памяти, а получаем их по мере необходимости. Это экономит память и время.

В реальной жизни вы постоянно используете итераторы, даже не задумываясь. Когда вы листаете ленту в Instagram — вы «итерируетесь» по постам. Когда смотрите сериал по одной серии — вы используете «ленивый» подход. А если бы вы скачали сразу все серии — это был бы «жадный» подход, который требует много места.

Теория: как это работает изнутри

Чтобы понять итераторы, нужно разобраться с двумя ключевыми понятиями: протокол итерирования и Symbol.iterator.

Протокол итерирования

В TypeScript (и JavaScript) любой объект может стать «итерируемым» (то есть таким, по которому можно пройтись циклом for...of), если он реализует специальный метод с ключом Symbol.iterator.

Этот метод должен вернуть объект-итератор, у которого есть метод next(). Метод next() возвращает объект с двумя полями:

  • value — текущее значение элемента
  • done — флаг, закончились ли элементы (true — да, false — ещё есть)

Визуально это выглядит так:

// Структура итератора
interface IteratorResult<T> {
    value: T | undefined;  // Текущее значение
    done: boolean;         // Закончились ли элементы?
}

interface Iterator<T> {
    next(): IteratorResult<T>;
}

// Структура итерируемого объекта
interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

Когда вы пишете for (let item of collection), TypeScript под капотом делает примерно следующее:

  1. Вызывает collection[Symbol.iterator]() — получает итератор
  2. Вызывает iterator.next() — получает первый элемент
  3. Если done === false — присваивает value переменной item и выполняет тело цикла
  4. Повторяет шаги 2-3, пока done !== true

Генераторы: как они работают

Генератор — это функция, которая помечена звёздочкой function*. Она не выполняется сразу, а возвращает специальный объект — Generator, который одновременно является и итератором, и итерируемым объектом.

Ключевое слово yield — это «пауза». Когда генератор доходит до yield, он:

  1. Возвращает значение, указанное после yield
  2. Приостанавливает своё выполнение
  3. Ждёт следующего вызова next()

Это похоже на чтение книги с закладками: вы дочитали до главы, поставили закладку (yield), закрыли книгу. Когда снова открываете — начинаете ровно с того места, где остановились.

🔬 Важное отличие: Обычная функция выполняется от начала до конца за один раз. Генератор может «замораживаться» и «размораживаться» сколько угодно раз.

Синтаксис в TypeScript: все формы и варианты

1. Создание итератора вручную

Вы можете сделать любой объект итерируемым, реализовав Symbol.iterator:

// Создаём объект, который будет итерируемым
const range = {
    from: 1,
    to: 5,
    
    // Реализуем метод Symbol.iterator
    [Symbol.iterator]() {
        let current = this.from;
        const last = this.to;
        
        // Возвращаем объект-итератор с методом next()
        return {
            next() {
                if (current <= last) {
                    // Возвращаем текущее значение и увеличиваем счётчик
                    return { value: current++, done: false };
                } else {
                    // Элементы закончились
                    return { value: undefined, done: true };
                }
            }
        };
    }
};

// Теперь можно использовать for...of
for (const num of range) {
    console.log(num); // 1, 2, 3, 4, 5
}

2. Объявление функции-генератора

// Функция-генератор помечается звёздочкой
function* generateSequence() {
    yield 1;  // Пауза: возвращаем 1
    yield 2;  // Пауза: возвращаем 2
    yield 3;  // Пауза: возвращаем 3
}

// Создаём генератор (функция не выполняется!)
const generator = generateSequence();

// Вызываем next() — функция выполняется до первого yield
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }

3. Генератор как итерируемый объект

Генератор сам является итерируемым, поэтому его можно использовать в for...of:

function* colors() {
    yield 'красный';
    yield 'синий';
    yield 'зелёный';
}

// for...of автоматически вызывает next() до done: true
for (const color of colors()) {
    console.log(color); // красный, синий, зелёный
}

// Можно развернуть в массив
const colorArray = [...colors()];
console.log(colorArray); // ['красный', 'синий', 'зелёный']

4. Типизация генераторов в TypeScript

// Явно указываем типы для Generator
// Generator<YieldType, ReturnType, NextType>
function* numberGenerator(): Generator<number, string, void> {
    yield 10;
    yield 20;
    yield 30;
    return 'Готово!'; // return завершает генератор
}

const gen = numberGenerator();
console.log(gen.next()); // { value: 10, done: false }
console.log(gen.next()); // { value: 20, done: false }
console.log(gen.next()); // { value: 30, done: false }
console.log(gen.next()); // { value: 'Готово!', done: true }

5. Передача значений в генератор через next()

function* interactive() {
    const name = yield 'Как тебя зовут?';  // Пауза: ждём имя
    const age = yield `Привет, ${name}! Сколько тебе лет?`;  // Пауза: ждём возраст
    yield `${name}, тебе ${age} лет — отлично!`;
}

const dialog = interactive();

console.log(dialog.next());          // { value: 'Как тебя зовут?', done: false }
console.log(dialog.next('Анна'));    // { value: 'Привет, Анна! Сколько тебе лет?', done: false }
console.log(dialog.next('25'));      // { value: 'Анна, тебе 25 лет — отлично!', done: false }
console.log(dialog.next());          // { value: undefined, done: true }

6. yield* — делегирование другому генератору

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

function* oneToFive() {
    yield* oneToThree();  // Делегируем: все yield из oneToThree
    yield 4;
    yield 5;
}

for (const num of oneToFive()) {
    console.log(num); // 1, 2, 3, 4, 5
}

7. Бесконечные генераторы

function* infiniteCounter() {
    let i = 0;
    while (true) {  // Бесконечный цикл — это нормально для генератора
        yield i++;
    }
}

const counter = infiniteCounter();
console.log(counter.next().value); // 0
console.log(counter.next().value); // 1
console.log(counter.next().value); // 2
// Можно вызывать бесконечно — память не переполняется

Примеры кода — от простого к сложному

Пример 1: Ручной итератор для массива

// Создаём свой итератор для массива
function createArrayIterator(arr: number[]) {
    let index = 0;
    
    return {
        next() {
            if (index < arr.length) {
                // Возвращаем элемент и увеличиваем индекс
                return { value: arr[index++], done: false };
            } else {
                // Массив закончился
                return { value: undefined, done: true };
            }
        }
    };
}

const numbers = [10, 20, 30];
const iterator = createArrayIterator(numbers);

console.log(iterator.next()); // { value: 10, done: false }
console.log(iterator.next()); // { value: 20, done: false }
console.log(iterator.next()); // { value: 30, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

Пример 2: Генератор для чисел Фибоначчи

// Генератор последовательности Фибоначчи
function* fibonacci(limit: number) {
    let a = 0;
    let b = 1;
    
    while (a <= limit) {
        yield a;  // Отдаём текущее число
        [a, b] = [b, a + b];  // Сдвигаем: следующее число = сумма двух предыдущих
    }
}

// Получаем числа Фибоначчи до 100
for (const num of fibonacci(100)) {
    console.log(num); // 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
}

// Можно собрать в массив
const fibArray = [...fibonacci(50)];
console.log(fibArray); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Пример 3: Генератор для чтения больших файлов

// Имитация чтения большого файла по строкам
function* readLines(text: string) {
    const lines = text.split('
');
    
    for (const line of lines) {
        // Можно обработать строку перед выдачей
        yield line.trim();
    }
}

const bigText = `Первая строка
Вторая строка
Третья строка
Четвёртая строка`;

// Читаем по одной строке, не загружая всё в память
const lineReader = readLines(bigText);

for (const line of lineReader) {
    console.log(`Обрабатываю: "${line}"`);
    // Здесь можно делать сложную обработку каждой строки
}

Пример 4: Генератор с состоянием (пароль)

// Генератор, который запоминает состояние между вызовами
function* passwordValidator() {
    let attempts = 0;
    const maxAttempts = 3;
    const correctPassword = 'secret123';
    
    while (attempts < maxAttempts) {
        const password = yield `Попытка ${attempts + 1}: Введите пароль`;
        
        if (password === correctPassword) {
            yield 'Доступ разрешён!';
            return;  // Завершаем генератор
        }
        
        attempts++;
        
        if (attempts < maxAttempts) {
            yield 'Неверный пароль. Попробуйте ещё раз.';
        } else {
            yield 'Доступ заблокирован. Слишком много попыток.';
        }
    }
}

const validator = passwordValidator();

console.log(validator.next().value);           // Попытка 1: Введите пароль
console.log(validator.next('wrong1').value);   // Неверный пароль. Попробуйте ещё раз.
console.log(validator.next().value);           // Попытка 2: Введите пароль
console.log(validator.next('secret123').value); // Доступ разрешён!

Пример 5: Комбинирование нескольких генераторов

// Генератор чётных чисел
function* evenNumbers(limit: number) {
    for (let i = 0; i <= limit; i += 2) {
        yield i;
    }
}

// Генератор нечётных чисел
function* oddNumbers(limit: number) {
    for (let i = 1; i <= limit; i += 2) {
        yield i;
    }
}

// Генератор, который чередует чётные и нечётные
function* alternatingNumbers(limit: number) {
    const evens = evenNumbers(limit);
    const odds = oddNumbers(limit);
    
    // Чередуем: чётное, нечётное, чётное, нечётное...
    for (let i = 0; i <= limit; i++) {
        if (i % 2 === 0) {
            const next = evens.next();
            if (!next.done) yield next.value;
        } else {
            const next = odds.next();
            if (!next.done) yield next.value;
        }
    }
}

console.log([...alternatingNumbers(10)]); 
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Пример 6: Генератор для обхода дерева (рекурсия)

// Структура дерева
interface TreeNode {
    value: number;
    children: TreeNode[];
}

// Генератор для обхода дерева в глубину
function* depthFirstTraversal(node: TreeNode): Generator<number> {
    // Сначала отдаём значение текущего узла
    yield node.value;
    
    // Затем рекурсивно обходим всех детей
    for (const child of node.children) {
        yield* depthFirstTraversal(child);  // Делегируем обход ребёнка
    }
}

// Создаём дерево
const tree: TreeNode = {
    value: 1,
    children: [
        {
            value: 2,
            children: [
                { value: 4, children: [] },
                { value: 5, children: [] }
            ]
        },
        {
            value: 3,
            children: [
                { value: 6, children: [] }
            ]
        }
    ]
};

// Обходим дерево
for (const val of depthFirstTraversal(tree)) {
    console.log(val); // 1, 2, 4, 5, 3, 6
}

Пример 7: Генератор с обработкой ошибок

// Генератор, который может выбрасывать исключения
function* safeDivision(dividends: number[], divisor: number) {
    for (const dividend of dividends) {
        try {
            if (divisor === 0) {
                throw new Error('Деление на ноль!');
            }
            yield dividend / divisor;
        } catch (error) {
            // Вместо того чтобы прерывать генератор, возвращаем null
            yield null;
        }
    }
}

const numbers = [10, 20, 0, 40, 50];
const divider = safeDivision(numbers, 0);

for (const result of divider) {
    if (result === null) {
        console.log('Ошибка деления');
    } else {
        console.log(`Результат: ${result}`);
    }
}
// Вывод: Ошибка деления (для всех, так как divisor = 0)

Сравнение с Python: чем похоже и чем отличается

Характеристика TypeScript Python
Объявление генератора function* gen() { yield x; } def gen(): yield x
Ключевое слово yield yield
Делегирование yield* otherGen() yield from other_gen()
Типизация Generator<T, TReturn, TNext> Динамическая (типы не указываются)
Итерация for...of for...in (для итераторов)
Создание итератора [Symbol.iterator]() __iter__()
Следующий элемент .next() next() (функция) или .__next__()
Исключения в генераторе generator.throw(error) generator.throw(error)
Закрытие генератора generator.return() generator.close()
Генераторные выражения Нет (используйте Array.from + генератор) (x for x in range(10))

Основные различия:

  • Синтаксис: В TypeScript используется function*, в Python — def с yield
  • Типизация: TypeScript требует указания типов, Python — динамический язык
  • Делегирование: В TS — yield*, в Python — yield from
  • Генераторные выражения: В Python есть краткая форма (x for x in list), в TS такой нет
💡 Совет: Если вы знаете Python, то освоить генераторы в TypeScript будет очень легко — концепция та же, отличается только синтаксис.

Типичные ошибки и подводные камни

Ошибка 1: Забыли вызвать генератор

function* myGen() {
    yield 1;
    yield 2;
}

// ❌ Ошибка: myGen — это функция, а не генератор
for (const val of myGen) {  // TypeError: myGen is not iterable
    console.log(val);
}

// ✅ Правильно: нужно вызвать функцию
for (const val of myGen()) {
    console.log(val);
}

Ошибка 2: Генератор нельзя переиспользовать

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

const gen = counter();

// Первый проход — всё работает
console.log([...gen]); // [1, 2, 3]

// ❌ Второй проход — пусто!
console.log([...gen]); // [] — генератор уже завершён

// ✅ Решение: создавать новый генератор каждый раз
console.log([...counter()]); // [1, 2, 3]
console.log([...counter()]); // [1, 2, 3]

Ошибка 3: Бесконечный цикл без условия выхода

// ❌ Опасный генератор — никогда не завершится
function* infinite() {
    while (true) {
        yield Math.random();
    }
}

// Если забыть break — цикл будет вечным
for (const num of infinite()) {
    console.log(num);
    // Нужно явно прервать
    if (num > 0.9) break;
}

// ✅ Безопасный вариант — с лимитом
function* finiteRandom(limit: number) {
    for (let i = 0; i < limit; i++) {
        yield Math.random();
    }
}

Ошибка 4: Путаница между return и yield

function* example() {
    yield 1;
    return 2;  // return завершает генератор
    yield 3;   // ❌ Этот yield никогда не выполнится
}

const gen = example();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: true } — return попал в value
console.log(gen.next()); // { value: undefined, done: true }

// В for...of return игнорируется
for (const val of example()) {
    console.log(val); // Только 1
}

Ошибка 5: Неправильная типизация генератора

// ❌ Слишком общая типизация
function* badGen(): Generator {
    yield 'строка';
    yield 42;  // TypeScript не ругается, но это может быть ошибкой
}

// ✅ Явно указываем типы
function* goodGen(): Generator<string | number, void, void> {
    yield 'строка';
    yield 42;
}

Лучшие практики и советы профессионалов

1. Используйте генераторы для работы с большими данными

Если вы обрабатываете файлы размером в гигабайты, генераторы — ваш лучший друг. Они позволяют обрабатывать данные по частям, не загружая всё в оперативную память.

// Чтение большого CSV-файла построчно
async function* readCSVLines(filePath: string) {
    const file = await fetch(filePath);
    const reader = file.body?.getReader();
    const decoder = new TextDecoder();
    
    let buffer = '';
    
    while (true) {
        const { done, value } = await reader!.read();
        
        if (done) {
            if (buffer) yield buffer;
            break;
        }
        
        buffer += decoder.decode(value, { stream: true });
        const lines = buffer.split('
');
        buffer = lines.pop() || '';  // Оставляем неполную строку в буфере
        
        for (const line of lines) {
            yield line;
        }
    }
}

2. Комбинируйте генераторы для создания пайплайнов

Генераторы отлично компонуются: выход одного может быть входом для другого.

// Пайплайн обработки данных
function* filterEven(numbers: Generator<number>) {
    for (const num of numbers) {
        if (num % 2 === 0) yield num;
    }
}

function* multiplyBy(factor: number, numbers: Generator<number>) {
    for (const num of numbers) {
        yield num * factor;
    }
}

function* take(limit: number, numbers: Generator<number>) {
    let count = 0;
    for (const num of numbers) {
        if (count++ >= limit) break;
        yield num;
    }
}

// Использование
function* naturalNumbers() {
    let i = 0;
    while (true) yield i++;
}

const pipeline = take(5, multiplyBy(10, filterEven(naturalNumbers())));
console.log([...pipeline]); // [0, 20, 40, 60, 80]

3. Используйте генераторы для создания состояний

Генераторы отлично подходят для реализации конечных автоматов и состояний.

// Конечный автомат: светофор
function* trafficLight() {
    while (true) {
        yield 'зелёный';
        yield 'жёлтый';
        yield 'красный';
    }
}

const light = trafficLight();
console.log(light.next().value); // зелёный
console.log(light.next().value); // жёлтый
console.log(light.next().value); // красный
console.log(light.next().value); // зелёный (цикл)

4. Не злоупотребляйте генераторами

Генераторы — мощный инструмент, но не всегда нужный. Для простых итераций по массиву используйте обычные методы forEach, map, filter. Генераторы оправданы, когда:

  • Данные поступают асинхронно или по частям
  • Нужно ленивое вычисление (бесконечные последовательности)
  • Нужно сохранять состояние между вызовами
  • Строится сложный пайплайн обработки

5. Всегда обрабатывайте ошибки

function* safeGenerator() {
    try {
        while (true) {
            const data = yield;
            // Обработка данных
        }
    } catch (error) {
        console.error('Генератор получил ошибку:', error);
    } finally {
        console.log('Генератор завершён');
    }
}

Вопросы для самопроверки

  1. Вопрос: Что такое итератор и чем он отличается от итерируемого объекта?

    Ответ: Итерируемый объект — это объект, у которого есть метод Symbol.iterator. Итератор — это объект, который возвращает этот метод и имеет метод next(). Итератор «знает», как получать следующий элемент, а итерируемый объект просто говорит: «Вот мой итератор».

  2. Вопрос: Какой символ используется для создания итератора в TypeScript?

    Ответ: Symbol.iterator. Это специальный символ, который TypeScript (и JavaScript) использует для определения протокола итерирования.

  3. Вопрос: В чём разница между yield и return в генераторе?

    Ответ: yield приостанавливает выполнение генератора и возвращает значение, но генератор можно продолжить. return завершает генератор навсегда — после него никакие yield не выполнятся. Значение return попадает в value при done: true.

  4. Вопрос: Можно ли использовать один и тот же генератор дважды?

    Ответ: Нет. После того как генератор завершился (все yield выполнены или вызван return), он становится «истощённым». Все последующие вызовы next() будут возвращать { value: undefined, done: true }. Нужно создать новый экземпляр генератора.

  5. Вопрос: Что такое yield* и зачем он нужен?

    Ответ: yield* делегирует выполнение другому генератору или итерируемому объекту. Это позволяет компоновать генераторы: один генератор может «передать управление» другому, и все значения второго генератора будут включены в последовательность первого.

  6. Вопрос: Как передать значение в генератор извне?

    Ответ: Через аргумент метода next(value). Это значение станет результатом выражения yield внутри генератора. Например: const name = yield 'Введите имя'; — значение, переданное в next(), попадёт в переменную name.

  7. Вопрос: В чём преимущество генераторов перед обычными массивами?

    Ответ: Генераторы используют ленивые вычисления — они не хранят все