Rebuilding Effect's Result.gen

Posted on Wed 19 August 2026 in Tech, TypeScript, Effect

Since I discovered ZIO I have been in love with effect systems. Effect brings an effect system to TypeScript, making it widely more useful due to the adoption of the language. I spent hours (probably days) digging into ZIO's codebase and learned a ton, it's now time to do the same with Effect's (v4) codebase.

My first focus was on higher-kinded types but in order to get there, we need a short detour: building a generator-based interpreter for the Result type.

what does yield* do?

My main objective here is to demystify the following example from Effect's documentation:

import { import ResultResult } from "effect"

const const maybeName: Result.Result<string, string>maybeName: import ResultResult.type Result<A, E = never> = Result.Success<A, E> | Result.Failure<A, E>

A value that is either Success<A, E> or Failure<A, E>.

When to use

Use when both success and failure should remain available as data and Option would lose failure information.

Details

  • Use succeed / fail to construct
  • Use match to fold both branches
  • Use isSuccess / isFailure to narrow the type

E defaults to never, so Result<number> means a result that cannot fail.

Example (Creating and matching a Result)

import { Result } from "effect"

Result.match(Result.succeed(42), {
  onSuccess: (value) => `Success: ${value}`,
  onFailure: (error) => `Error: ${error}`
}) // => "Success: 42"

Namespace containing type-level utilities for extracting the inner types of a Result.

Example (Extracting inner types)

import { Result } from "effect"

type R = Result.Result<number, string>

// number
type A = Result.Result.Success<R>

// string
type E = Result.Result.Failure<R>

const success: A = 42
const failure: E = "error"
@seesucceed / fail to create values@seematch to fold both branches@seeisSuccess / isFailure for type guards@categorymodels@since4.0.0@since4.0.0
Result
<string, string> = import ResultResult.const succeed: <string>(right: string) => Result.Result<string, never>

Creates a Result holding a Success value.

Details

  • Use when you have a value and want to lift it into the Result type
  • The error type E defaults to never

Example (Wrapping a value)

import { Result } from "effect"

Result.succeed(42) // => Result.succeed(42)
@seefail to create a Failure@seevoid_void for a pre-built Success<void>@categoryconstructors@since4.0.0
succeed
("ex0ns")
const const maybeAge: Result.Result<number, string>maybeAge: import ResultResult.type Result<A, E = never> = Result.Success<A, E> | Result.Failure<A, E>

A value that is either Success<A, E> or Failure<A, E>.

When to use

Use when both success and failure should remain available as data and Option would lose failure information.

Details

  • Use succeed / fail to construct
  • Use match to fold both branches
  • Use isSuccess / isFailure to narrow the type

E defaults to never, so Result<number> means a result that cannot fail.

Example (Creating and matching a Result)

import { Result } from "effect"

Result.match(Result.succeed(42), {
  onSuccess: (value) => `Success: ${value}`,
  onFailure: (error) => `Error: ${error}`
}) // => "Success: 42"

Namespace containing type-level utilities for extracting the inner types of a Result.

Example (Extracting inner types)

import { Result } from "effect"

type R = Result.Result<number, string>

// number
type A = Result.Result.Success<R>

// string
type E = Result.Result.Failure<R>

const success: A = 42
const failure: E = "error"
@seesucceed / fail to create values@seematch to fold both branches@seeisSuccess / isFailure for type guards@categorymodels@since4.0.0@since4.0.0
Result
<number, string> = import ResultResult.const succeed: <number>(right: number) => Result.Result<number, never>

Creates a Result holding a Success value.

Details

  • Use when you have a value and want to lift it into the Result type
  • The error type E defaults to never

Example (Wrapping a value)

import { Result } from "effect"

Result.succeed(42) // => Result.succeed(42)
@seefail to create a Failure@seevoid_void for a pre-built Success<void>@categoryconstructors@since4.0.0
succeed
(25)
const
const program: Result.Result<{
    name: string;
    age: number;
}, string>
program
= import ResultResult.
const gen: <unknown, Result.Result<string, string> | Result.Result<number, string>, {
    name: string;
    age: number;
}>(...args: [self: unknown, body: (this: unknown) => Generator<Result.Result<string, string> | Result.Result<number, string>, {
    name: string;
    age: number;
}, never>] | [body: () => Generator<Result.Result<string, string> | Result.Result<number, string>, {
    name: string;
    age: number;
}, never>]) => Result.Result<{
    name: string;
    age: number;
}, string>

Provides generator-based syntax for composing Result values sequentially.

When to use

Use when you need generator syntax to compose sequential Result computations instead of nested flatMap calls.

Details

  • Use yield* to unwrap a Result inside the generator; if any yielded Result is a Failure, the generator short-circuits and returns that failure
  • The return value of the generator is wrapped in Success
  • Evaluated eagerly and synchronously (unlike Effect.gen)

Example (Composing multiple Results)

import { Result } from "effect"

Result.gen(function*() {
  const a = yield* Result.succeed(1)
  const b = yield* Result.succeed(2)
  return a + b
}) // => Result.succeed(3)
@seeflatMap for point-free sequential composition@seeall to collect multiple independent Results@categorygenerators@since2.0.0
gen
(function* () {
const const name: stringname = (yield* const maybeName: Result.Result<string, string>maybeName).String.toUpperCase(): string

Converts all the alphabetic characters in a string to uppercase.

toUpperCase
()
const const age: numberage = yield* const maybeAge: Result.Result<number, string>maybeAge return { name: stringname, age: numberage } }) var console: Consoleconsole.Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(
const program: Result.Result<{
    name: string;
    age: number;
}, string>
program
)
const program: Result.Result<{
    name: string;
    age: number;
}, string>
program
// => Result.succeed({ name: "ex0ns", age: 25 })

a minimal Result

Let's start with a simple definition for our container:

interface interface Success<A, E>Success<function (type parameter) A in Success<A, E>A, function (type parameter) E in Success<A, E>E> {
  readonly Success<A, E>._tag: "Success"_tag: "Success";
  readonly Success<A, E>.value: Avalue: function (type parameter) A in Success<A, E>A;
}

interface interface Failure<A, E>Failure<function (type parameter) A in Failure<A, E>A, function (type parameter) E in Failure<A, E>E> {
  readonly Failure<A, E>._tag: "Failure"_tag: "Failure";
  readonly Failure<A, E>.error: Eerror: function (type parameter) E in Failure<A, E>E;
}

type type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<function (type parameter) A in type Result<A, E = never>A, function (type parameter) E in type Result<A, E = never>E = never> = interface Success<A, E>Success<function (type parameter) A in type Result<A, E = never>A, function (type parameter) E in type Result<A, E = never>E> | interface Failure<A, E>Failure<function (type parameter) A in type Result<A, E = never>A, function (type parameter) E in type Result<A, E = never>E>;

At the type level this is all we need to get started. Let's now create a few methods to build values:

function function succeed<A>(value: A): Result<A, never>succeed<function (type parameter) A in succeed<A>(value: A): Result<A, never>A>(value: Avalue: function (type parameter) A in succeed<A>(value: A): Result<A, never>A): type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<function (type parameter) A in succeed<A>(value: A): Result<A, never>A, never> {
    return {
        Success<A, never>._tag: "Success"_tag: "Success",
        Success<A, never>.value: Avalue: value: Avalue
    }
}


function function fail<E>(error: E): Result<never, E>fail<function (type parameter) E in fail<E>(error: E): Result<never, E>E>(error: Eerror: function (type parameter) E in fail<E>(error: E): Result<never, E>E): type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<never, function (type parameter) E in fail<E>(error: E): Result<never, E>E> {
    return {
        Failure<A, E>._tag: "Failure"_tag: "Failure",
        Failure<never, E>.error: Eerror: error: Eerror
    }
}

const successValue = function succeed<number>(value: number): Result<number, never>succeed(1)
const successValue: Result<number, never>
const failedValue =
function fail<{
    details: string;
}>(error: {
    details: string;
}): Result<never, {
    details: string;
}>
fail
({details: stringdetails: "something wrong happened"})
const failedValue: Result<never, {
    details: string;
}>

making Result work with yield*

This works well, but it is not very useful yet: we lack composition. We can't merge Results together. We need to implement map and flatmap.

And while Effect implements those, I'm going to focus on the more interesting part of effect: generator-based composition. There are a few requirements for this to work, so we now need to dive into iterators:

function* function add(): Generator<any, any, unknown>add() {
    const const a: anya = yield* succeed(1);
Type 'Result<number, never>' must have a '[Symbol.iterator]()' method that returns an iterator.
const const b: anyb = yield* function succeed<number>(value: number): Result<number, never>succeed(2); return const a: anya + const b: anyb; }

The previous snippet is failing because yield* requires its operand to be iterable, for this we need to add [Symbol.iterator]() to our object. Our iterator produces two kinds of results: it first yields the complete Result<A, E> wrapper, then returns an A, which becomes the value of the yield* expression.

TypeScript represents these two possible outcomes as IteratorResult<TYield, TReturn>. Here, TYield is Result<A, E> and TReturn is A.

We are first going to define a few type helpers to improve readability:

type type SuccessValue<T> = T extends Result<infer A, any> ? A : neverSuccessValue<function (type parameter) T in type SuccessValue<T>T> = function (type parameter) T in type SuccessValue<T>T extends type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<infer function (type parameter) AA, any> ? function (type parameter) AA : never;

interface interface ResultIterator<T extends Result<any, any>>ResultIterator<function (type parameter) T in ResultIterator<T extends Result<any, any>>T extends type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<any, any>> {
  ResultIterator<T extends Result<any, any>>.next(...args: ReadonlyArray<any>): IteratorResult<T, SuccessValue<T>>next(...args: readonly any[]args: interface ReadonlyArray<T>ReadonlyArray<any>): type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>IteratorResult<function (type parameter) T in ResultIterator<T extends Result<any, any>>T, type SuccessValue<T> = T extends Result<infer A, any> ? A : neverSuccessValue<function (type parameter) T in ResultIterator<T extends Result<any, any>>T>>;
}

declare const const value: numbervalue: type SuccessValue<T> = T extends Result<infer A, any> ? A : neverSuccessValue<interface Success<A, E>Success<number, never>>
declare const const error: nevererror: type SuccessValue<T> = T extends Result<infer A, any> ? A : neverSuccessValue<interface Failure<A, E>Failure<never, string>>

SuccessValue lets us unpack the type of the success branch from a Result, while ResultIterator describes the type of the iterator returned by [Symbol.iterator]().

We can now add an iterator to our Result type, and Typescript is now happy with our type.

interface interface Success<A, E>Success<function (type parameter) A in Success<A, E>A, function (type parameter) E in Success<A, E>E> {
  readonly Success<A, E>._tag: "Success"_tag: "Success";
  readonly Success<A, E>.value: Avalue: function (type parameter) A in Success<A, E>A;
  [var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iterator

A method that returns the default iterator for an object. Called by the semantics of the for-of statement.

iterator
](): interface ResultIterator<T extends Result<any, any>>ResultIterator<type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<function (type parameter) A in Success<A, E>A, function (type parameter) E in Success<A, E>E>>
} interface interface Failure<A, E>Failure<function (type parameter) A in Failure<A, E>A, function (type parameter) E in Failure<A, E>E> { readonly Failure<A, E>._tag: "Failure"_tag: "Failure"; readonly Failure<A, E>.error: Eerror: function (type parameter) E in Failure<A, E>E; [var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iterator

A method that returns the default iterator for an object. Called by the semantics of the for-of statement.

iterator
](): interface ResultIterator<T extends Result<any, any>>ResultIterator<type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<function (type parameter) A in Failure<A, E>A, function (type parameter) E in Failure<A, E>E>>
} declare const const a: Success<number, never>a: interface Success<A, E>Success<number, never> declare const const b: Success<number, never>b: interface Success<A, E>Success<number, never> function *function add(): Generator<Result<number, never>, number, any>add() { const valueA = yield* const a: Success<number, never>a;
const valueA: number
const valueB = yield* const b: Success<number, never>b;
const valueB: number
return const valueA: numbervalueA+const valueB: numbervalueB; }

However, we need to update our constructors to account for this new field:

function function succeed<A>(value: A): Result<A, never>succeed<function (type parameter) A in succeed<A>(value: A): Result<A, never>A>(value: Avalue: function (type parameter) A in succeed<A>(value: A): Result<A, never>A): type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<function (type parameter) A in succeed<A>(value: A): Result<A, never>A, never> {
    const const self: Success<A, never>self: interface Success<A, E>Success<function (type parameter) A in succeed<A>(value: A): Result<A, never>A, never> = {
        Success<A, never>._tag: "Success"_tag: "Success",
        Success<A, never>.value: Avalue: value: Avalue,
        [var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iterator

A method that returns the default iterator for an object. Called by the semantics of the for-of statement.

iterator
]() {
// Not implemented yet } } return const self: Success<A, never>self; } function function fail<E>(error: E): Result<never, E>fail<function (type parameter) E in fail<E>(error: E): Result<never, E>E>(error: Eerror: function (type parameter) E in fail<E>(error: E): Result<never, E>E): type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<never, function (type parameter) E in fail<E>(error: E): Result<never, E>E> { const const self: Failure<never, E>self: interface Failure<A, E>Failure<never, function (type parameter) E in fail<E>(error: E): Result<never, E>E> = { Failure<never, E>._tag: "Failure"_tag: "Failure", Failure<never, E>.error: Eerror: error: Eerror, [var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iterator

A method that returns the default iterator for an object. Called by the semantics of the for-of statement.

iterator
]() {
// Not implemented yet } } return const self: Failure<never, E>self; }

Now we need to implement [Symbol.iterator](). It returns an iterator with two steps:

  • next() yields the complete Result.
  • next(value) finishes with value, which becomes the value of the yield* expression.

Effect calls this iterator SingleShotGen but I decided to call it SingleShotIterator to avoid the confusion between the generator language feature (the object returned by a generator function) and the Gen suffix that Effect uses for its gen methods (we will see that in a bit).

class class SingleShotIterator<T, A>SingleShotIterator<function (type parameter) T in SingleShotIterator<T, A>T, function (type parameter) A in SingleShotIterator<T, A>A> implements interface IterableIterator<T, TReturn = any, TNext = any>

Describes a user-defined Iterator that is also iterable.

IterableIterator
<function (type parameter) T in SingleShotIterator<T, A>T, function (type parameter) A in SingleShotIterator<T, A>A> {
private SingleShotIterator<T, A>.called: booleancalled: boolean = false; private readonly SingleShotIterator<T, A>.self: Tself: function (type parameter) T in SingleShotIterator<T, A>T; constructor(self: Tself: function (type parameter) T in SingleShotIterator<T, A>T) { this.SingleShotIterator<T, A>.self: Tself = self: Tself; } SingleShotIterator<T, A>.next(value: A): IteratorResult<T, A>next(value: Avalue: function (type parameter) A in SingleShotIterator<T, A>A): type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>IteratorResult<function (type parameter) T in SingleShotIterator<T, A>T, function (type parameter) A in SingleShotIterator<T, A>A> { if (this.SingleShotIterator<T, A>.called: booleancalled) { return { IteratorReturnResult<A>.value: Avalue, IteratorReturnResult<A>.done: truedone: true }; } this.SingleShotIterator<T, A>.called: booleancalled = true; return { IteratorYieldResult<T>.value: Tvalue: this.SingleShotIterator<T, A>.self: Tself, IteratorYieldResult<T>.done?: false | undefineddone: false }; } [var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iterator

A method that returns the default iterator for an object. Called by the semantics of the for-of statement.

iterator
](): interface IterableIterator<T, TReturn = any, TNext = any>

Describes a user-defined Iterator that is also iterable.

IterableIterator
<function (type parameter) T in SingleShotIterator<T, A>T, function (type parameter) A in SingleShotIterator<T, A>A> {
return new constructor SingleShotIterator<T, A>(self: T): SingleShotIterator<T, A>SingleShotIterator<function (type parameter) T in SingleShotIterator<T, A>T, function (type parameter) A in SingleShotIterator<T, A>A>(this.SingleShotIterator<T, A>.self: Tself); } }

Let's spend some time processing what is happening here: we are building an IterableIterator, it's an Iterator as it implements its protocol (having a next method returning a value and a status), and it's also an Iterable as it has a [Symbol.iterator] returning a new Iterator.

The first time next is called, it returns the value passed to the constructor (in our case this will be Result<A,E>), the second time the next function is called, it returns the parameter that was passed. It means that whoever is consuming this iterator will have to call it once, unpack the result, and call next a second time with the unpacked value.

Now let's plug this Iterator into our constructors:

function function succeed<A>(value: A): Result<A, never>succeed<function (type parameter) A in succeed<A>(value: A): Result<A, never>A>(value: Avalue: function (type parameter) A in succeed<A>(value: A): Result<A, never>A): type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<function (type parameter) A in succeed<A>(value: A): Result<A, never>A, never> {
    const const self: Success<A, never>self: interface Success<A, E>Success<function (type parameter) A in succeed<A>(value: A): Result<A, never>A, never> = {
        Success<A, never>._tag: "Success"_tag: "Success",
        Success<A, never>.value: Avalue: value: Avalue,
        [var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iterator

A method that returns the default iterator for an object. Called by the semantics of the for-of statement.

iterator
]() {
return new constructor SingleShotIterator<Result<A, never>, A>(self: Result<A, never>): SingleShotIterator<Result<A, never>, A>SingleShotIterator<type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<function (type parameter) A in succeed<A>(value: A): Result<A, never>A>, function (type parameter) A in succeed<A>(value: A): Result<A, never>A>(const self: Success<A, never>self); } } return const self: Success<A, never>self; } function function fail<E>(error: E): Result<never, E>fail<function (type parameter) E in fail<E>(error: E): Result<never, E>E>(error: Eerror: function (type parameter) E in fail<E>(error: E): Result<never, E>E): type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<never, function (type parameter) E in fail<E>(error: E): Result<never, E>E> { const const self: Failure<never, E>self: interface Failure<A, E>Failure<never, function (type parameter) E in fail<E>(error: E): Result<never, E>E> = { Failure<never, E>._tag: "Failure"_tag: "Failure", Failure<never, E>.error: Eerror: error: Eerror, [var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iterator

A method that returns the default iterator for an object. Called by the semantics of the for-of statement.

iterator
]() {
return new constructor SingleShotIterator<Result<never, E>, never>(self: Result<never, E>): SingleShotIterator<Result<never, E>, never>SingleShotIterator<type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<never, function (type parameter) E in fail<E>(error: E): Result<never, E>E>, never>(const self: Failure<never, E>self); } } return const self: Failure<never, E>self; }

We are passing the Result as a value to the constructor of SingleShotIterator. And with that our first test with real values:

function* function add(): Generator<Result<number, never>, number, any>add() {
    const const a: numbera = yield* function succeed<number>(value: number): Result<number, never>succeed(1);
    const const b: numberb = yield* function succeed<number>(value: number): Result<number, never>succeed(2);
    return const a: numbera + const b: numberb;
}

driving the generator

We now have a problem as our result is trapped within this generator and we don't have a convenient way to consume its output. To read the final result of the generator we need the following code:

function* add() {
function add(): Generator<Result<number, never>, number, any>
const const a: numbera = yield* function succeed<number>(value: number): Result<number, never>succeed(1); const const b: numberb = yield* function succeed<number>(value: number): Result<number, never>succeed(2); return const a: numbera + const b: numberb; } const const generator: Generator<Result<number, never>, number, any>generator = function add(): Generator<Result<number, never>, number, any>add(); const const a: IteratorResult<Result<number, never>, number>a = const generator: Generator<Result<number, never>, number, any>generator.Generator<Result<number, never>, number, any>.next(...[value]: [] | [any]): IteratorResult<Result<number, never>, number>next(); if(!const a: IteratorResult<Result<number, never>, number>a.done?: boolean | undefineddone && const a: IteratorYieldResult<Result<number, never>>a.IteratorYieldResult<Result<number, never>>.value: Result<number, never>value._tag: "Success" | "Failure"_tag === "Success") { const const b: IteratorResult<Result<number, never>, number>b = const generator: Generator<Result<number, never>, number, any>generator.Generator<Result<number, never>, number, any>.next(...[value]: [] | [any]): IteratorResult<Result<number, never>, number>next(const a: IteratorYieldResult<Result<number, never>>a.IteratorYieldResult<Result<number, never>>.value: Success<number, never>value.Success<number, never>.value: numbervalue); if(!const b: IteratorResult<Result<number, never>, number>b.done?: boolean | undefineddone && const b: IteratorYieldResult<Result<number, never>>b.IteratorYieldResult<Result<number, never>>.value: Result<number, never>value._tag: "Success" | "Failure"_tag === "Success") { const const result: IteratorResult<Result<number, never>, number>result = const generator: Generator<Result<number, never>, number, any>generator.Generator<Result<number, never>, number, any>.next(...[value]: [] | [any]): IteratorResult<Result<number, never>, number>next(const b: IteratorYieldResult<Result<number, never>>b.IteratorYieldResult<Result<number, never>>.value: Success<number, never>value.Success<number, never>.value: numbervalue); if(const result: IteratorResult<Result<number, never>, number>result.done?: boolean | undefineddone) { var console: Consoleconsole.Console.log(...data: any[]): void

The console.log() static method outputs a message to the console.

MDN Reference

log
(const result: IteratorReturnResult<number>result.value)
IteratorReturnResult<number>.value: number
} } }

This cannot scale: we need to support an arbitrary number of yields, mixed success and error types. In order to solve this problem we have to introduce an interpreter that's going to perform all of that for us, in Effect those usually are functions called gen. For our Result type the interpreter will be fairly easy:

  • create a generator
  • as long as the iterator is not done, consume values from it, if we receive a Failure, stop there and return it, otherwise unpack the value and send it back (generator.next(a.value.value))
  • wrap and return the final value
type type ErrorOf<K> = K extends never ? never : K extends Result<any, infer E> ? E : neverErrorOf<function (type parameter) K in type ErrorOf<K>K> = function (type parameter) K in type ErrorOf<K>K extends never ? never
  : function (type parameter) K in type ErrorOf<K>K extends type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<any, infer function (type parameter) EE>
    ? function (type parameter) EE
    : never;

function function gen<K extends Result<any, any>, A>(generator: () => Generator<K, A, any>): Result<A, ErrorOf<K>>gen<function (type parameter) K in gen<K extends Result<any, any>, A>(generator: () => Generator<K, A, any>): Result<A, ErrorOf<K>>K extends type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<any, any>, function (type parameter) A in gen<K extends Result<any, any>, A>(generator: () => Generator<K, A, any>): Result<A, ErrorOf<K>>A>(
  generator: () => Generator<K, A, any>generator: () => interface Generator<T = unknown, TReturn = any, TNext = any>Generator<function (type parameter) K in gen<K extends Result<any, any>, A>(generator: () => Generator<K, A, any>): Result<A, ErrorOf<K>>K, function (type parameter) A in gen<K extends Result<any, any>, A>(generator: () => Generator<K, A, any>): Result<A, ErrorOf<K>>A, any>,
): type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<function (type parameter) A in gen<K extends Result<any, any>, A>(generator: () => Generator<K, A, any>): Result<A, ErrorOf<K>>A, type ErrorOf<K> = K extends never ? never : K extends Result<any, infer E> ? E : neverErrorOf<function (type parameter) K in gen<K extends Result<any, any>, A>(generator: () => Generator<K, A, any>): Result<A, ErrorOf<K>>K>> {
  const const body: Generator<K, A, any>body = generator: () => Generator<K, A, any>generator();
  let let next: IteratorResult<K, A>next = const body: Generator<K, A, any>body.Generator<K, A, any>.next(...[value]: [] | [any]): IteratorResult<K, A>next();

  while (!let next: IteratorResult<K, A>next.done?: boolean | undefineddone) {
    if (let next: IteratorYieldResult<K>next.IteratorYieldResult<K>.value: Result<any, any>value._tag: "Success" | "Failure"_tag === "Failure") return let next: IteratorYieldResult<K>next.IteratorYieldResult<K>.value: Failure<any, any>value;
    let next: IteratorResult<K, A>next = const body: Generator<K, A, any>body.Generator<K, A, any>.next(...[value]: [] | [any]): IteratorResult<K, A>next(let next: IteratorYieldResult<K>next.IteratorYieldResult<K>.value: Success<any, any>value.Success<any, any>.value: anyvalue);
  }

  return function succeed<A>(value: A): Result<A, never>succeed(let next: IteratorReturnResult<A>next.IteratorReturnResult<A>.value: Avalue);
}

function* add() {
function add(): Generator<Result<number, never>, number, any>
const const a: numbera = yield* function succeed<number>(value: number): Result<number, never>succeed(1); const const b: numberb = yield* function succeed<number>(value: number): Result<number, never>succeed(2); return const a: numbera + const b: numberb; } const result = function gen<Result<number, never>, number>(generator: () => Generator<Result<number, never>, number, any>): Result<number, never>gen(function add(): Generator<Result<number, never>, number, any>add)
const result: Result<number, never>

Note that while the body of the method is very close to Effect's actual implementation, the type signature is not. The real type signature is more complex as it's a generic type that can be used for multiple generators. In this example ErrorOf is a shortcut I took that is specific to Result.

tracing the generator

If we trace the execution, the following things are happening:

01 caller
02 gen
03 add
04 iterator
Operation

In other words: yield* exposes the complete Result to gen so the interpreter can inspect it. A Failure stops the computation and a Success is unwrapped and passed back with body.next(value). The delegated iterator then returns that value, making the yield* expression evaluate to A.

One question I had when writing this code was: why yielding the full container (Result<A, E>) rather than directly returning the value?

The answer is, I believe, pretty straightforward:

  • you can share SingleShotIterator between different container (Effect, Result, Option)
  • you might not have a value to return (yield* fail("error"))

Now let's run our original example from the doc:

const const maybeName: Result<string, string>maybeName: type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<string, string> = function succeed<string>(value: string): Result<string, never>succeed("ex0ns")
const const maybeAge: Result<number, string>maybeAge: type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<number, string> = function succeed<number>(value: number): Result<number, never>succeed(25)

const program = 
function gen<Success<string, string> | Failure<string, string> | Success<number, string>, {
    name: string;
    age: number;
}>(generator: () => Generator<Success<string, string> | Failure<string, string> | Success<number, string>, {
    name: string;
    age: number;
}, any>): Result<{
    name: string;
    age: number;
}, string>
gen
(function* () {
const program: Result<{
    name: string;
    age: number;
}, string>
const const name: stringname = (yield* const maybeName: Result<string, string>maybeName).String.toUpperCase(): string

Converts all the alphabetic characters in a string to uppercase.

toUpperCase
()
const const age: numberage = yield* const maybeAge: Result<number, string>maybeAge return { name: stringname, age: numberage } }) const error = function gen<Result<never, "first error"> | Result<never, "second error">, void>(generator: () => Generator<Result<never, "first error"> | Result<never, "second error">, void, any>): Result<void, "first error" | "second error">gen(function* () {
const error: Result<void, "first error" | "second error">
yield* function fail<"first error">(error: "first error"): Result<never, "first error">fail("first error" as type const = "first error"const) yield* function fail<"second error">(error: "second error"): Result<never, "second error">fail("second error" as type const = "second error"const) })

And just like that we have a working interpreter for our custom Result type!

the same trick powers Option.gen

We started with a very simple looking yield* succeed("ex0ns") expression and figured out how it works and why it works the way it does. yield* delegates to a single-shot iterator, that iterator exposes the complete Result to gen, and gen decides whether to stop or send the success value back. The really cool thing is that it's the same protocol for Option.gen. Its interpreter stops on None instead of Failure but the flow between yield*, the iterator, and gen remains the exact same:

export type type ValueOf<T extends Option<any>> = T extends Option<infer _A> ? _A : neverValueOf<function (type parameter) T in type ValueOf<T extends Option<any>>T extends type Option<A> = Some<A> | None<A>Option<any>> = function (type parameter) T in type ValueOf<T extends Option<any>>T extends type Option<A> = Some<A> | None<A>Option<infer function (type parameter) _A_A> ? function (type parameter) _A_A : never
export interface interface OptionIterator<T extends Option<any>>OptionIterator<function (type parameter) T in OptionIterator<T extends Option<any>>T extends type Option<A> = Some<A> | None<A>Option<any>> {
  OptionIterator<T extends Option<any>>.next(...args: ReadonlyArray<any>): IteratorResult<T, ValueOf<T>>next(
    ...args: readonly any[]args: interface ReadonlyArray<T>ReadonlyArray<any>
  ): type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>IteratorResult<function (type parameter) T in OptionIterator<T extends Option<any>>T, type ValueOf<T extends Option<any>> = T extends Option<infer _A> ? _A : neverValueOf<function (type parameter) T in OptionIterator<T extends Option<any>>T>>
}

interface interface Some<A>Some<function (type parameter) A in Some<A>A> {
  readonly Some<A>._tag: "Some"_tag: "Some";
  readonly Some<A>.value: Avalue: function (type parameter) A in Some<A>A;
  [var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iterator

A method that returns the default iterator for an object. Called by the semantics of the for-of statement.

iterator
](): interface OptionIterator<T extends Option<any>>OptionIterator<type Option<A> = Some<A> | None<A>Option<function (type parameter) A in Some<A>A>>
} interface interface None<A>None<function (type parameter) A in None<A>A> { readonly None<A>._tag: "None"_tag: "None"; [var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iterator

A method that returns the default iterator for an object. Called by the semantics of the for-of statement.

iterator
](): interface OptionIterator<T extends Option<any>>OptionIterator<type Option<A> = Some<A> | None<A>Option<function (type parameter) A in None<A>A>>
} type type Option<A> = Some<A> | None<A>Option<function (type parameter) A in type Option<A>A> = interface Some<A>Some<function (type parameter) A in type Option<A>A> | interface None<A>None<function (type parameter) A in type Option<A>A>; function function some<A>(value: A): Option<A>some<function (type parameter) A in some<A>(value: A): Option<A>A>(value: Avalue: function (type parameter) A in some<A>(value: A): Option<A>A): type Option<A> = Some<A> | None<A>Option<function (type parameter) A in some<A>(value: A): Option<A>A> { const const self: Some<A>self: interface Some<A>Some<function (type parameter) A in some<A>(value: A): Option<A>A> = { Some<A>._tag: "Some"_tag: "Some", Some<A>.value: Avalue: value: Avalue, [var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iterator

A method that returns the default iterator for an object. Called by the semantics of the for-of statement.

iterator
]() {
return new constructor SingleShotIterator<Option<A>, A>(self: Option<A>): SingleShotIterator<Option<A>, A>SingleShotIterator<type Option<A> = Some<A> | None<A>Option<function (type parameter) A in some<A>(value: A): Option<A>A>, function (type parameter) A in some<A>(value: A): Option<A>A>(const self: Some<A>self); } } return const self: Some<A>self; } function function none<A = never>(): Option<A>none<function (type parameter) A in none<A = never>(): Option<A>A = never>(): type Option<A> = Some<A> | None<A>Option<function (type parameter) A in none<A = never>(): Option<A>A> { const const self: None<A>self: interface None<A>None<function (type parameter) A in none<A = never>(): Option<A>A> = { None<A>._tag: "None"_tag: "None", [var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iterator

A method that returns the default iterator for an object. Called by the semantics of the for-of statement.

iterator
]() {
return new constructor SingleShotIterator<Option<A>, never>(self: Option<A>): SingleShotIterator<Option<A>, never>SingleShotIterator<type Option<A> = Some<A> | None<A>Option<function (type parameter) A in none<A = never>(): Option<A>A>, never>(const self: None<A>self); } } return const self: None<A>self; } function function gen<K extends Option<any>, A>(generator: () => Generator<K, A, any>): Option<A>gen<function (type parameter) K in gen<K extends Option<any>, A>(generator: () => Generator<K, A, any>): Option<A>K extends type Option<A> = Some<A> | None<A>Option<any>, function (type parameter) A in gen<K extends Option<any>, A>(generator: () => Generator<K, A, any>): Option<A>A>( generator: () => Generator<K, A, any>generator: () => interface Generator<T = unknown, TReturn = any, TNext = any>Generator<function (type parameter) K in gen<K extends Option<any>, A>(generator: () => Generator<K, A, any>): Option<A>K, function (type parameter) A in gen<K extends Option<any>, A>(generator: () => Generator<K, A, any>): Option<A>A, any>, ): type Option<A> = Some<A> | None<A>Option<function (type parameter) A in gen<K extends Option<any>, A>(generator: () => Generator<K, A, any>): Option<A>A> { const const body: Generator<K, A, any>body = generator: () => Generator<K, A, any>generator(); let let next: IteratorResult<K, A>next = const body: Generator<K, A, any>body.Generator<K, A, any>.next(...[value]: [] | [any]): IteratorResult<K, A>next(); while (!let next: IteratorResult<K, A>next.done?: boolean | undefineddone) { if (let next: IteratorYieldResult<K>next.IteratorYieldResult<K>.value: Option<any>value._tag: "Some" | "None"_tag === "None") return let next: IteratorYieldResult<K>next.IteratorYieldResult<K>.value: None<any>value; let next: IteratorResult<K, A>next = const body: Generator<K, A, any>body.Generator<K, A, any>.next(...[value]: [] | [any]): IteratorResult<K, A>next(let next: IteratorYieldResult<K>next.IteratorYieldResult<K>.value: Some<any>value.Some<any>.value: anyvalue); } return function some<A>(value: A): Option<A>some(let next: IteratorReturnResult<A>next.IteratorReturnResult<A>.value: Avalue); } const const someName: Option<string>someName = function some<string>(value: string): Option<string>some("ex0ns") const const someAge: Option<number>someAge = function some<number>(value: number): Option<number>some(34) const program =
function gen<Some<string> | None<string> | Some<number>, {
    name: string;
    age: number;
}>(generator: () => Generator<Some<string> | None<string> | Some<number>, {
    name: string;
    age: number;
}, any>): Option<{
    name: string;
    age: number;
}>
gen
(function* () {
const program: Option<{
    name: string;
    age: number;
}>
const const name: stringname = (yield* const someName: Option<string>someName).String.toUpperCase(): string

Converts all the alphabetic characters in a string to uppercase.

toUpperCase
()
const const age: numberage = yield* const someAge: Option<number>someAge return { name: stringname, age: numberage } }) const error = function gen<Option<never>, never>(generator: () => Generator<Option<never>, never, any>): Option<never>gen(function* () {
const error: Option<never>
return yield* function none<never>(): Option<never>none() })

Effect's implementation of Some and None is a bit more complex than that but the idea behind it is virtually the same.

As you can see the type signature of the gen method is almost exactly the same, we mecanically replaced Result with Option. Effect goes further and describes both functions using a generic type called Gen<F>: this is what I want to cover in the next.

disclaimer

This article was written in my own words. I used LLMs to correct my English and generate the interactive demo.

This article reflects my personal understanding of Effect and has not been reviewed by anyone from the Effect team.