Learning Book

Генераторы и типизация yield

Генераторы и типизация yield

Функции-генераторы

Функция-генератор (function*) – это функция, которая может приостанавливать своё выполнение с помощью yield и возобновлять его позже. Каждый вызов next() выполняет код до следующего yield.

function* countdown(start: number): Generator<number, string, unknown> {
  for (let i = start; i > 0; i--) {
    yield i;
  }
  return "Готово!";
}

const gen = countdown(3);

console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: "Готово!", done: true }

Генераторы автоматически реализуют протокол IterableIterator, поэтому их можно использовать в for...of:

for (const n of countdown(3)) {
  console.log(n); // 3, 2, 1 (return-значение "Готово!" не попадает в for...of)
}

Тип Generator<T, TReturn, TNext>

TypeScript описывает генераторы тремя параметрами типа:

interface Generator<T = unknown, TReturn = any, TNext = unknown>
  extends Iterator<T, TReturn, TNext> {
  next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
  return(value: TReturn): IteratorResult<T, TReturn>;
  throw(e: any): IteratorResult<T, TReturn>;
  [Symbol.iterator](): Generator<T, TReturn, TNext>;
}
ПараметрОписаниеГде проявляется
T (yield type)Тип значений, которые генератор отдаёт через yieldgen.next().value когда done: false
TReturnТип значения, которое генератор возвращает через returngen.next().value когда done: true
TNextТип значения, которое передаётся в генератор через next(value)Аргумент gen.next(value)

Пример с передачей значений в генератор

function* accumulator(): Generator<number, number, number> {
  let total = 0;
  while (true) {
    // yield отдаёт текущий total и принимает следующее значение
    const input: number = yield total;
    total += input;
  }
}

const acc = accumulator();
acc.next();           // { value: 0, done: false } -- первый next() запускает генератор
acc.next(10);         // { value: 10, done: false }
acc.next(20);         // { value: 30, done: false }
console.log(acc.next(5)); // { value: 35, done: false }

Первый вызов next() всегда запускает генератор до первого yield – передаваемый аргумент игнорируется. Последующие вызовы next(value) передают value как результат выражения yield.

Вывод типов

TypeScript умеет выводить тип генератора автоматически:

// Тип: Generator<number, void, unknown>
function* fibonacci() {
  let a = 0;
  let b = 1;
  while (true) {
    yield a;           // T = number (выводится из yield)
    [a, b] = [b, a + b];
  }
  // Нет return -- TReturn = void
  // next() не принимает значений -- TNext = unknown
}

Если нужна точность, можно указать тип явно:

function* typedGen(): Generator<string, boolean, number> {
  const input: number = yield "начало";
  const input2: number = yield `получено: ${input}`;
  return input2 > 10;
}

Делегирование через yield*

Оператор yield* передаёт управление другому итерируемому объекту или генератору. Все значения делегированного генератора «проходят через» текущий:

function* inner(): Generator<string, number, unknown> {
  yield "a";
  yield "b";
  return 42; // return-значение доступно через yield*
}

function* outer(): Generator<string | number, void, unknown> {
  yield 1;
  const result: number = yield* inner(); // делегирование
  console.log(`inner вернул: ${result}`); // 42
  yield 2;
}

const values = [...outer()];
console.log(values); // [1, "a", "b", 2]
// Обратите внимание: return-значение 42 НЕ попадает в итерацию

yield* можно использовать с любым Iterable:

function* flatten<T>(arrays: Iterable<T[]>): Generator<T> {
  for (const arr of arrays) {
    yield* arr; // делегирование массиву
  }
}

const flat = [...flatten([[1, 2], [3, 4], [5]])];
console.log(flat); // [1, 2, 3, 4, 5]

Генераторы как конечные автоматы

Генераторы идеально подходят для описания конечных автоматов (state machines), где каждый yield – это переход между состояниями:

type TrafficLight = "red" | "yellow" | "green";

function* trafficLightCycle(): Generator<TrafficLight, never, unknown> {
  while (true) {
    yield "red";
    yield "green";
    yield "yellow";
  }
}

const light = trafficLightCycle();
console.log(light.next().value); // "red"
console.log(light.next().value); // "green"
console.log(light.next().value); // "yellow"
console.log(light.next().value); // "red" -- цикл повторяется

Более сложный пример с состоянием:

interface ConnectionState {
  status: "disconnected" | "connecting" | "connected" | "error";
  message: string;
}

type ConnectionAction = "connect" | "disconnect" | "retry";

function* connectionManager(): Generator<ConnectionState, void, ConnectionAction> {
  let action: ConnectionAction;

  while (true) {
    // Состояние: отключён
    action = yield { status: "disconnected", message: "Ожидание подключения" };

    if (action === "connect") {
      // Состояние: подключение
      action = yield { status: "connecting", message: "Подключение..." };

      // Имитация: 50% шанс ошибки
      if (Math.random() > 0.5) {
        action = yield { status: "connected", message: "Подключено" };

        if (action === "disconnect") {
          continue; // Назад к disconnected
        }
      } else {
        action = yield { status: "error", message: "Ошибка подключения" };

        if (action === "retry") {
          continue; // Назад к disconnected, затем попробует снова
        }
      }
    }
  }
}

Асинхронные генераторы

Асинхронные генераторы (async function*) объединяют async/await и yield. Они реализуют интерфейс AsyncGenerator<T, TReturn, TNext> и итерируются через for await...of:

interface AsyncGenerator<T = unknown, TReturn = any, TNext = unknown> {
  next(...args: [] | [TNext]): Promise<IteratorResult<T, TReturn>>;
  return(value: TReturn): Promise<IteratorResult<T, TReturn>>;
  throw(e: any): Promise<IteratorResult<T, TReturn>>;
  [Symbol.asyncIterator](): AsyncGenerator<T, TReturn, TNext>;
}
async function* fetchPages(urls: string[]): AsyncGenerator<string> {
  for (const url of urls) {
    const response = await fetch(url);
    const text = await response.text();
    yield text; // Каждая страница отдаётся по мере загрузки
  }
}

async function processPages(): Promise<void> {
  const urls = [
    "https://example.com/page1",
    "https://example.com/page2",
    "https://example.com/page3",
  ];

  for await (const html of fetchPages(urls)) {
    console.log(`Получено ${html.length} символов`);
  }
}

Symbol.asyncIterator

Аналог Symbol.iterator для асинхронных итерируемых объектов:

class AsyncRange implements AsyncIterable<number> {
  constructor(
    private start: number,
    private end: number,
    private delayMs: number
  ) {}

  async *[Symbol.asyncIterator](): AsyncGenerator<number> {
    for (let i = this.start; i <= this.end; i++) {
      await new Promise((resolve) => setTimeout(resolve, this.delayMs));
      yield i;
    }
  }
}

async function main(): Promise<void> {
  // Числа от 1 до 5 с задержкой 500мс
  for await (const n of new AsyncRange(1, 5, 500)) {
    console.log(n);
  }
}

Паттерн: потоковое чтение данных

Асинхронные генераторы идеальны для потоковой обработки:

async function* readLines(
  reader: ReadableStreamDefaultReader<Uint8Array>
): AsyncGenerator<string> {
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();

    if (done) {
      if (buffer.length > 0) {
        yield buffer;
      }
      return;
    }

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";

    for (const line of lines) {
      yield line;
    }
  }
}

Целевой стандарт для генераторов

Для полноценной поддержки генераторов и асинхронных генераторов в TypeScript:

// tsconfig.json
{
  "compilerOptions": {
    // Для синхронных генераторов: ES2015+
    // Для for await...of: ES2018+
    // Для асинхронных генераторов: ES2018+
    "target": "ES2018",
    "lib": ["ES2018"]
  }
}

Типизация yield* и вывод TReturn

При делегировании через yield* TypeScript извлекает тип TReturn делегированного генератора:

function* innerGen(): Generator<string, number, unknown> {
  yield "hello";
  return 100;
}

function* outerGen(): Generator&lt;string | boolean, void, unknown&gt; {
  // TypeScript знает, что result: number (TReturn innerGen)
  const result: number = yield* innerGen();
  yield result > 50; // boolean
}

Это работает благодаря тому, что yield* возвращает значение return делегированного генератора. TypeScript корректно выводит этот тип даже в сложных цепочках делегирования.

Однако если делегируемый объект – не генератор, а обычный Iterable, то yield* возвращает void:

function* delegateToArray(): Generator<number, void, unknown> {
  const result: void = yield* [1, 2, 3]; // Iterable не имеет TReturn
  console.log(result); // undefined
}