Generators
Tsonic supports JavaScript generator functions with full bidirectional communication. This guide covers basic generators, async generators, and the advanced bidirectional patterns.
Overview
Generator functions are declared with function* and use yield to produce values:
import { Console } from "@tsonic/dotnet/System.js";
function* counter(): Generator<number> {
yield 1;
yield 2;
yield 3;
}
for (const n of counter()) {
Console.WriteLine(n); // 1, 2, 3
}
Generator Type Signature
The full generator type is Generator<TYield, TReturn, TNext>:
| Type Parameter | Description | Default |
|---|---|---|
TYield |
Type of values yielded by the generator | Required |
TReturn |
Type of the final return value | void |
TNext |
Type of values passed to next() |
undefined |
Basic Generator (Yield Only)
When you only need to yield values:
function* range(start: number, end: number): Generator<number> {
for (let i = start; i <= end; i++) {
yield i;
}
}
Generator with Return Value
When the generator returns a final value:
function* countdown(n: number): Generator<number, string> {
while (n > 0) {
yield n;
n--;
}
return "Liftoff!";
}
Bidirectional Generator
When the generator receives values from the caller:
function* accumulator(start: number): Generator<number, void, number> {
let total = start;
while (true) {
const received = yield total;
total += received;
}
}
Using Generators
Iteration
Use for...of to iterate over yielded values:
import { Console } from "@tsonic/dotnet/System.js";
function* fibonacci(): Generator<number> {
let a = 0,
b = 1;
while (a < 100) {
yield a;
[a, b] = [b, a + b];
}
}
for (const n of fibonacci()) {
Console.WriteLine(n); // 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89
}
Manual Iteration with next()
Use .next() for manual control:
import { Console } from "@tsonic/dotnet/System.js";
function* counter(): Generator<number> {
yield 1;
yield 2;
yield 3;
}
const gen = counter();
Console.WriteLine(gen.next().value); // 1
Console.WriteLine(gen.next().value); // 2
Console.WriteLine(gen.next().value); // 3
Console.WriteLine(gen.next().done); // true
IteratorResult
Each next() call returns an IteratorResult<T>:
interface IteratorResult<T> {
value: T;
done: boolean;
}
done: false- Generator yielded a valuedone: true- Generator has completed
Bidirectional Communication
Sending Values with next(value)
Pass values into the generator using next(value):
import { Console } from "@tsonic/dotnet/System.js";
function* accumulator(start: number): Generator<number, void, number> {
let total = start;
while (true) {
const received = yield total;
total = total + received;
}
}
export function main(): void {
const gen = accumulator(0);
// First next() starts the generator - value is ignored
Console.WriteLine(gen.next().value); // 0
// Subsequent next(value) passes value to generator
Console.WriteLine(gen.next(5).value); // 5
Console.WriteLine(gen.next(10).value); // 15
Console.WriteLine(gen.next(3).value); // 18
}
First next() Semantics
The value passed to the first next() call is always ignored (per JavaScript spec):
function* echo(): Generator<string, void, string> {
while (true) {
const msg = yield "ready";
yield `Echo: ${msg}`;
}
}
const gen = echo();
gen.next("ignored"); // First call - "ignored" is discarded
// Returns: { value: "ready", done: false }
gen.next("hello"); // "hello" is received
// Returns: { value: "Echo: hello", done: false }
Practical Example: Data Processor
import { Console } from "@tsonic/dotnet/System.js";
function* dataProcessor(): Generator<string, number, number> {
let sum = 0;
let count = 0;
while (true) {
const value = yield `Received ${count} values, sum = ${sum}`;
if (value < 0) {
return sum; // Negative value signals end
}
sum = sum + value;
count = count + 1;
}
}
export function main(): void {
const processor = dataProcessor();
Console.WriteLine(processor.next().value); // "Received 0 values, sum = 0"
Console.WriteLine(processor.next(10).value); // "Received 1 values, sum = 10"
Console.WriteLine(processor.next(20).value); // "Received 2 values, sum = 30"
Console.WriteLine(processor.next(5).value); // "Received 3 values, sum = 35"
const final = processor.next(-1);
Console.WriteLine(`Done: ${final.done}`); // "Done: true"
}
Generator Control Methods
next(value?)
Advances the generator to the next yield point:
const gen = myGenerator();
const result = gen.next(); // Start or resume
const result2 = gen.next(42); // Resume with value 42
return(value)
Terminates the generator with a specified return value:
import { Console } from "@tsonic/dotnet/System.js";
function* counter(): Generator<number, string> {
let i = 0;
while (true) {
yield i++;
}
}
const gen = counter();
Console.WriteLine(gen.next().value); // 0
Console.WriteLine(gen.next().value); // 1
gen.return("done"); // Terminates generator
Console.WriteLine(gen.next().done); // true
Note: The value passed to return() becomes the generator's return value but does NOT appear in the IteratorResult.value. Access it via the returnValue property (Tsonic extension).
throw(error) - Limitation
The throw() method terminates the generator, but does NOT inject the exception at the yield point like JavaScript does.
// This JavaScript pattern does NOT work the same in Tsonic:
function* withTryCatch(): Generator<number> {
try {
yield 1;
yield 2;
} catch (e) {
yield -1; // In JS, gen.throw() would resume here
}
}
const gen = withTryCatch();
gen.next(); // { value: 1, done: false }
gen.throw(Error()); // In JS: { value: -1, done: false }
// In Tsonic: throws immediately, no catch
This is a fundamental limitation of C# iterators which don't support resumption with exceptions.
Return Values
Capturing Return Values
Generators can return a final value using return:
function* countdown(n: number): Generator<number, string, number> {
while (n > 0) {
const step = yield n;
n = n - (step > 0 ? step : 1);
}
return "Liftoff!";
}
Accessing Return Values
The return value is available after the generator completes:
import { Console } from "@tsonic/dotnet/System.js";
const gen = countdown(3);
Console.WriteLine(gen.next().value); // 3
Console.WriteLine(gen.next(1).value); // 2
Console.WriteLine(gen.next(1).value); // 1
const final = gen.next(1);
Console.WriteLine(final.done); // true
// final.value in JS would be "Liftoff!"
Tsonic Extension: Use the returnValue property to access the return value:
const returnValue = gen.returnValue; // "Liftoff!"
Async Generators
Basic Async Generator
Use async function* for asynchronous generators:
import { Console } from "@tsonic/dotnet/System.js";
async function* fetchPages(): AsyncGenerator<string> {
for (let page = 1; page <= 3; page++) {
await delay(100);
yield `Page ${page}`;
}
}
export async function main(): Promise<void> {
for await (const page of fetchPages()) {
Console.WriteLine(page);
}
}
Bidirectional Async Generator
Async generators also support bidirectional communication:
import { Console } from "@tsonic/dotnet/System.js";
async function* asyncAccumulator(
start: number
): AsyncGenerator<number, number, number> {
let total = start;
while (true) {
const received = yield total;
total = total + received;
}
}
export async function main(): Promise<void> {
const gen = asyncAccumulator(10);
const r1 = await gen.next();
Console.WriteLine(`Initial: ${r1.value}`); // 10
const r2 = await gen.next(5);
Console.WriteLine(`After +5: ${r2.value}`); // 15
const r3 = await gen.next(20);
Console.WriteLine(`After +20: ${r3.value}`); // 35
}
For-Await Loops
Use for await...of to iterate over async generators:
import { Console } from "@tsonic/dotnet/System.js";
async function* asyncRange(start: number, end: number): AsyncGenerator<number> {
for (let i = start; i <= end; i++) {
await delay(100);
yield i;
}
}
export async function main(): Promise<void> {
for await (const n of asyncRange(1, 5)) {
Console.WriteLine(n); // 1, 2, 3, 4, 5 (with delays)
}
}
Yield Delegation
Use yield* to delegate to another generator:
import { Console } from "@tsonic/dotnet/System.js";
function* inner(): Generator<number> {
yield 1;
yield 2;
}
function* outer(): Generator<number> {
yield 0;
yield* inner(); // Delegate to inner
yield 3;
}
for (const n of outer()) {
Console.WriteLine(n); // 0, 1, 2, 3
}
Async Yield Delegation
Async generators can delegate to other async iterables:
async function* asyncInner(): AsyncGenerator<string> {
yield "a";
yield "b";
}
async function* asyncOuter(): AsyncGenerator<string> {
yield "start";
yield* asyncInner();
yield "end";
}
Generated C# Code
Tsonic generates wrapper classes for bidirectional generators. Understanding this helps with debugging.
Simple Generator
function* counter(): Generator<number> {
yield 1;
yield 2;
}
Generates a simple IEnumerable<double>:
public static IEnumerable<double> counter()
{
yield return 1.0;
yield return 2.0;
}
Bidirectional Generator
function* accumulator(): Generator<number, void, number> {
let total = 0;
while (true) {
const v = yield total;
total += v;
}
}
Generates a wrapper class with exchange object:
// Exchange object for bidirectional communication
public sealed class accumulator_exchange
{
public double? Input { get; set; }
public double Output { get; set; }
}
// Wrapper class providing next(), return(), throw()
public sealed class accumulator_Generator
{
private readonly IEnumerator<accumulator_exchange> _enumerator;
private readonly accumulator_exchange _exchange;
private bool _done = false;
public IteratorResult<double> next(double? value = default) { ... }
public IteratorResult<double> @return(object? value = default) { ... }
public IteratorResult<double> @throw(object e) { ... }
}
Common Patterns
Infinite Sequence
import { Console } from "@tsonic/dotnet/System.js";
function* naturals(): Generator<number> {
let n = 0;
while (true) {
yield n++;
}
}
// Take first 5
const gen = naturals();
for (let i = 0; i < 5; i++) {
Console.WriteLine(gen.next().value);
}
State Machine
type State = "idle" | "running" | "stopped";
function* stateMachine(): Generator<State, void, string> {
let state: State = "idle";
while (true) {
const command = yield state;
if (command === "start" && state === "idle") {
state = "running";
} else if (command === "stop" && state === "running") {
state = "stopped";
} else if (command === "reset") {
state = "idle";
}
}
}
Coroutine Communication
function* producer(): Generator<number, void, void> {
for (let i = 0; i < 5; i++) {
yield i;
}
}
function* consumer(gen: Generator<number>): Generator<void, number, void> {
let sum = 0;
for (const n of gen) {
sum += n;
yield;
}
return sum;
}
Limitations
.throw()doesn't inject exceptions - Exceptions are thrown externally, not at yield point- No generator delegation return values -
yield*doesn't capture delegated return values - Type restrictions - TNext must be compatible with C# nullable types
See Also
- Async Patterns - Async/await and for-await loops
- Language Reference - Full language feature list
- .NET Interop - Working with .NET async