Iterators and Generators in TypeScript: A Detailed Guide
Что это такое и зачем? (аналогия из реальной жизни)
Представьте, что вы — библиотекарь, и вам нужно переписать все книги с полки. У вас есть два подхода:
Первый подход (без итератора): Вы снимаете все книги сразу, складываете их огромной кучей на столе и только потом начинаете переписывать. Проблема: стол может не вместить все книги, а если книг миллион — вы просто не сможете их поднять.
Второй подход (с итератором): Вы подходите к полке, берёте первую книгу, переписываете её, ставите на место. Затем берёте вторую — и так, пока не дойдёте до последней. Вы не храните всё в памяти одновременно, а работаете с каждой книгой по очереди.
Итератор — это как раз такой «библиотечный помощник», который умеет ходить по коллекции (массиву, списку, строке) и выдавать элементы по одному. Он помнит, на каком элементе остановился, и может сказать: «Есть ещё элементы?» или «Дай следующий».
Генератор — это фабрика по производству итераторов. Если итератор — это курьер, который приносит вам пиццу по одной, то генератор — это пиццерия, которая может испечь новую пиццу по вашему запросу. Генератор сам решает, когда остановиться и что отдать следующим.
💡 Важно: Итераторы и генераторы решают одну главную задачу — ленивые вычисления. Мы не храним все данные в памяти, а получаем их по мере необходимости. Это экономит память и время.
В реальной жизни вы постоянно используете итераторы, даже не задумываясь. Когда вы листаете ленту в 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 под капотом делает примерно следующее:
- Вызывает
collection[Symbol.iterator]()— получает итератор - Вызывает
iterator.next()— получает первый элемент - Если
done === false— присваиваетvalueпеременнойitemи выполняет тело цикла - Повторяет шаги 2-3, пока
done !== true
Генераторы: как они работают
Генератор — это функция, которая помечена звёздочкой function*. Она не выполняется сразу, а возвращает специальный объект — Generator, который одновременно является и итератором, и итерируемым объектом.
Ключевое слово yield — это «пауза». Когда генератор доходит до yield, он:
- Возвращает значение, указанное после
yield - Приостанавливает своё выполнение
- Ждёт следующего вызова
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('Генератор завершён');
}
}
Вопросы для самопроверки
-
Вопрос: Что такое итератор и чем он отличается от итерируемого объекта?
Ответ: Итерируемый объект — это объект, у которого есть метод
Symbol.iterator. Итератор — это объект, который возвращает этот метод и имеет методnext(). Итератор «знает», как получать следующий элемент, а итерируемый объект просто говорит: «Вот мой итератор». -
Вопрос: Какой символ используется для создания итератора в TypeScript?
Ответ:
Symbol.iterator. Это специальный символ, который TypeScript (и JavaScript) использует для определения протокола итерирования. -
Вопрос: В чём разница между
yieldиreturnв генераторе?Ответ:
yieldприостанавливает выполнение генератора и возвращает значение, но генератор можно продолжить.returnзавершает генератор навсегда — после него никакиеyieldне выполнятся. Значениеreturnпопадает вvalueприdone: true. -
Вопрос: Можно ли использовать один и тот же генератор дважды?
Ответ: Нет. После того как генератор завершился (все
yieldвыполнены или вызванreturn), он становится «истощённым». Все последующие вызовыnext()будут возвращать{ value: undefined, done: true }. Нужно создать новый экземпляр генератора. -
Вопрос: Что такое
yield*и зачем он нужен?Ответ:
yield*делегирует выполнение другому генератору или итерируемому объекту. Это позволяет компоновать генераторы: один генератор может «передать управление» другому, и все значения второго генератора будут включены в последовательность первого. -
Вопрос: Как передать значение в генератор извне?
Ответ: Через аргумент метода
next(value). Это значение станет результатом выраженияyieldвнутри генератора. Например:const name = yield 'Введите имя';— значение, переданное вnext(), попадёт в переменнуюname. -
Вопрос: В чём преимущество генераторов перед обычными массивами?
Ответ: Генераторы используют ленивые вычисления — они не хранят все