Higher kinded types for TypeScript

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

In the previous post we saw that Result.gen and Option.gen are very similar but looking at the Effect's codebase we can see that the type signature is very different from ours:

import { import ResultResult, import OptionOption, import UtilsUtils } from "effect"

declare const const genResult: Utils.Gen<Result.ResultTypeLambda>genResult: import UtilsUtils.type Gen<F extends TypeLambda> = <Self, K extends Utils.Variance<F, any, any, any> | Kind<F, any, any, any, any>, A>(...args: [self: Self, body: (this: Self) => Generator<K, A, never>] | [body: () => Generator<K, A, never>]) => Kind<F, [K] extends [Utils.Variance<F, infer R, any, any>] ? R : [K] extends [Kind<F, infer R, any, any, any>] ? R : never, [K] extends [Utils.Variance<F, any, infer O, any>] ? O : [K] extends [Kind<F, any, infer O, any, any>] ? O : never, [K] extends [Utils.Variance<F, any, any, infer E>] ? E : [K] extends [Kind<F, any, any, infer E, any>] ? E : never, A>

Type-level signature for generator-based monadic composition over any TypeLambda.

When to use

Use to type the gen function of a module that supports generator syntax, such as Option.gen, Result.gen, and Effect.gen.

Details

This is a pure type alias with no runtime behavior. It infers R, O, and E from the yielded values via Variance or Kind constraints. The generator's return type A becomes the output's A parameter.

Example (Typing a gen function for Option)

import { Option } from "effect"
import type { Utils } from "effect"

const gen: Utils.Gen<Option.OptionTypeLambda> = Option.gen
const result = gen(function*() {
  return yield* Option.some(1)
})
result // => Option.some(1)
@seeVariance for encoding the variance used for inference@seeSingleShotGen for the iterator protocol that makes yielding work@categorymodels@since2.0.0
Gen
<import ResultResult.ResultTypeLambda>
declare const const genOption: Utils.Gen<Option.OptionTypeLambda>genOption: import UtilsUtils.type Gen<F extends TypeLambda> = <Self, K extends Utils.Variance<F, any, any, any> | Kind<F, any, any, any, any>, A>(...args: [self: Self, body: (this: Self) => Generator<K, A, never>] | [body: () => Generator<K, A, never>]) => Kind<F, [K] extends [Utils.Variance<F, infer R, any, any>] ? R : [K] extends [Kind<F, infer R, any, any, any>] ? R : never, [K] extends [Utils.Variance<F, any, infer O, any>] ? O : [K] extends [Kind<F, any, infer O, any, any>] ? O : never, [K] extends [Utils.Variance<F, any, any, infer E>] ? E : [K] extends [Kind<F, any, any, infer E, any>] ? E : never, A>

Type-level signature for generator-based monadic composition over any TypeLambda.

When to use

Use to type the gen function of a module that supports generator syntax, such as Option.gen, Result.gen, and Effect.gen.

Details

This is a pure type alias with no runtime behavior. It infers R, O, and E from the yielded values via Variance or Kind constraints. The generator's return type A becomes the output's A parameter.

Example (Typing a gen function for Option)

import { Option } from "effect"
import type { Utils } from "effect"

const gen: Utils.Gen<Option.OptionTypeLambda> = Option.gen
const result = gen(function*() {
  return yield* Option.some(1)
})
result // => Option.some(1)
@seeVariance for encoding the variance used for inference@seeSingleShotGen for the iterator protocol that makes yielding work@categorymodels@since2.0.0
Gen
<import OptionOption.OptionTypeLambda>

So while the implementation is not the same between Result and Option, the type is a generic shared one. My goal here is to explain the mecanism behind that and show why it was needed and how they worked around TypeScript's limitation to make it work.

From my understanding of the codebase, Utils.Gen is actually not used anywhere else than in Result and Option. And its immediate value is pretty low (the type quite complex type for only two consumers). My guess is that it started as an experiment and was abandonned (or never adopted) in every type that uses a requirements channel (the third type parameter of Effect). This remains nonetheless interesting to study.

As a reminder, let's have a look at our own signatures from the previous post:

declare function function genResult<K extends Result<any, any>, A>(g: () => Generator<K, A, any>): Result<A, ErrorOf<K>>genResult<function (type parameter) K in genResult<K extends Result<any, any>, A>(g: () => 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 genResult<K extends Result<any, any>, A>(g: () => Generator<K, A, any>): Result<A, ErrorOf<K>>A>(g: () => Generator<K, A, any>g: () => interface Generator<T = unknown, TReturn = any, TNext = any>Generator<function (type parameter) K in genResult<K extends Result<any, any>, A>(g: () => Generator<K, A, any>): Result<A, ErrorOf<K>>K, function (type parameter) A in genResult<K extends Result<any, any>, A>(g: () => 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 genResult<K extends Result<any, any>, A>(g: () => 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 genResult<K extends Result<any, any>, A>(g: () => Generator<K, A, any>): Result<A, ErrorOf<K>>K>>
declare function function genOption<K extends Option<any>, A>(g: () => Generator<K, A, any>): Option<A>genOption<function (type parameter) K in genOption<K extends Option<any>, A>(g: () => Generator<K, A, any>): Option<A>K extends type Option<A> = Some<A> | None<A>Option<any>,      function (type parameter) A in genOption<K extends Option<any>, A>(g: () => Generator<K, A, any>): Option<A>A>(g: () => Generator<K, A, any>g: () => interface Generator<T = unknown, TReturn = any, TNext = any>Generator<function (type parameter) K in genOption<K extends Option<any>, A>(g: () => Generator<K, A, any>): Option<A>K, function (type parameter) A in genOption<K extends Option<any>, A>(g: () => Generator<K, A, any>): Option<A>A, any>): type Option<A> = Some<A> | None<A>Option<function (type parameter) A in genOption<K extends Option<any>, A>(g: () => Generator<K, A, any>): Option<A>A>

We can definitely see a common pattern in the types here and our goal at the end of this post will be to match Effect's implementation of Utils.gen.

type and type constructor

For the rest of this article, it's very important to distinguish a type from a type constructor. Result<number, string> is a type, a value can have this type, we can assign something to it. On the other hand, Result and Option are not types but type constructors. You should think of them as a function at the type level: give them one or more types, they will hand you back a type.

So our next obvious step would be to change the signature of gen to work with a type constructor. If we were to introduce a new parameter to abstract Result and Option (the type constructors) we could have a generic signature for the gen:

declare function function gen<F, K extends F<any, any>, A>(g: () => Generator<K, A, any>): F<A, ErrorOf<K>>gen<function (type parameter) F in gen<F, K extends F<any, any>, A>(g: () => Generator<K, A, any>): F<A, ErrorOf<K>>F, function (type parameter) K in gen<F, K extends F<any, any>, A>(g: () => Generator<K, A, any>): F<A, ErrorOf<K>>K extends F<any, any>, function (type parameter) A in gen<F, K extends F<any, any>, A>(g: () => Generator<K, A, any>): F<A, ErrorOf<K>>A>(g: () => Generator<K, A, any>g: () => interface Generator<T = unknown, TReturn = any, TNext = any>Generator<function (type parameter) K in gen<F, K extends F<any, any>, A>(g: () => Generator<K, A, any>): F<A, ErrorOf<K>>K, function (type parameter) A in gen<F, K extends F<any, any>, A>(g: () => Generator<K, A, any>): F<A, ErrorOf<K>>A, any>): F<A, ErrorOf<K>>
Type 'F' is not generic.
Type 'F' is not generic.

This very unhelpful error message here is telling us that a type parameter in TS must be a type, and can't be a type constructor. Another obvious problem is the number of type parameters, Result requires two while Option only has one, so this is definitely not going to work.

hiding a type constructor inside a type

As Typescript can do amazing things with types, if we can manage to represent a type constructor inside of a type, we should be able to use it inside the signature of gen.

In the following snippet, this is exactly what we are doing:

interface OptionLambda {
  readonly OptionLambda.Target: unknownTarget: unknown
  readonly OptionLambda.type: Option<this["Target"]>type: type Option<A> = Some<A> | None<A>Option<this["Target"]>
}

type 
type Apply<F extends { readonly type: unknown; }, Target> = (F & {
    readonly Target: Target;
})["type"]
Apply
<function (type parameter) F in type Apply<F extends { readonly type: unknown; }, Target>F extends { readonly type: unknowntype: unknown }, function (type parameter) Target in type Apply<F extends { readonly type: unknown; }, Target>Target> = (function (type parameter) F in type Apply<F extends { readonly type: unknown; }, Target>F & { readonly type Target: TargetTarget: function (type parameter) Target in type Apply<F extends { readonly type: unknown; }, Target>Target })["type"]
type A =
type Apply<F extends { readonly type: unknown; }, Target> = (F & {
    readonly Target: Target;
})["type"]
Apply
<OptionLambda, string>
type A = Some<string> | None<string>
// or with an even simpler example interface interface Box<A>Box<function (type parameter) A in Box<A>A> { readonly Box<A>.value: Avalue: function (type parameter) A in Box<A>A } interface BoxLambda { readonly BoxLambda.Target: unknownTarget: unknown; readonly BoxLambda.type: Box<this["Target"]>type: interface Box<A>Box<this["Target"]> } type B =
type Apply<F extends { readonly type: unknown; }, Target> = (F & {
    readonly Target: Target;
})["type"]
Apply
<BoxLambda, string>
type B = Box<string>

Let's take some time to process what we are doing here:

  • F has one constraint: it must contain a field called type. This is required for the final ["type"] access to work
  • Target is a placeholder, it only exists so type can exist.
  • type is a type, but is in reality waiting for Target to be filled in.

The trick here is that this refers to the type it is currently instantiated as. By itself OptionLambda["type"] is Option<unknown> but by intersecting it with {Target: string}, this["Target"] becomes unknown & string which is equal to string (unknown can always be removed from intersection) and type becomes Option<string>

What we built here is a type lambda and it describes: Target => Option<Target>, or Target => Box<Target> for the second one.

Note that Apply does not exist in Effect, but it's the simplest type one could define to showcase our new type lambda.

building gen for option

type 
type Apply<F extends { readonly type: unknown; }, Target> = (F & {
    readonly Target: Target;
})["type"]
Apply
<function (type parameter) F in type Apply<F extends { readonly type: unknown; }, Target>F extends { readonly type: unknowntype: unknown }, function (type parameter) Target in type Apply<F extends { readonly type: unknown; }, Target>Target> = (function (type parameter) F in type Apply<F extends { readonly type: unknown; }, Target>F & { readonly type Target: TargetTarget: function (type parameter) Target in type Apply<F extends { readonly type: unknown; }, Target>Target })["type"]
interface interface Gen<F extends { readonly type: unknown; }>Gen<function (type parameter) F in Gen<F extends { readonly type: unknown; }>F extends { readonly type: unknowntype: unknown }> { <function (type parameter) K in <K extends Apply<F, any>, A>(g: () => Generator<K, A, any>): Apply<F, A>K extends
type Apply<F extends { readonly type: unknown; }, Target> = (F & {
    readonly Target: Target;
})["type"]
Apply
<function (type parameter) F in Gen<F extends { readonly type: unknown; }>F, any>, function (type parameter) A in <K extends Apply<F, any>, A>(g: () => Generator<K, A, any>): Apply<F, A>A>(g: () => Generator<K, A, any>g: () => interface Generator<T = unknown, TReturn = any, TNext = any>Generator<function (type parameter) K in <K extends Apply<F, any>, A>(g: () => Generator<K, A, any>): Apply<F, A>K, function (type parameter) A in <K extends Apply<F, any>, A>(g: () => Generator<K, A, any>): Apply<F, A>A, any>):
type Apply<F extends { readonly type: unknown; }, Target> = (F & {
    readonly Target: Target;
})["type"]
Apply
<function (type parameter) F in Gen<F extends { readonly type: unknown; }>F, function (type parameter) A in <K extends Apply<F, any>, A>(g: () => Generator<K, A, any>): Apply<F, A>A>
} const const gen: Gen<OptionLambda>gen: interface Gen<F extends { readonly type: unknown; }>Gen<OptionLambda> = function optionGen<K extends Option<any>, A>(generator: () => Generator<K, A, any>): Option<A>optionGen // optionGen is our original implementation of `gen` for `Option`: the implementation did not change, we only care about typing here. function* program() {
function program(): Generator<Some<number> | None<number> | Some<string>, readonly [number, string], any>
const const a: numbera = yield* function some<number>(value: number): Option<number>some(1) const const b: stringb = yield* function some<string>(value: string): Option<string>some("two") return [const a: numbera, const b: stringb] as type const = readonly [number, string]const } const result =
const gen: Gen
<Some<number> | None<number> | Some<string>, readonly [number, string]>(g: () => Generator<Some<number> | None<number> | Some<string>, readonly [number, string], any>) => Option<readonly [number, string]>
gen
(function program(): Generator<Some<number> | None<number> | Some<string>, readonly [number, string], any>program)
const result: Option<readonly [number, string]>
you may have noticed something odd in the inferred type of program: some(1) and some("two") are yielded the same way, yet the union contains None<number> and no None<string>. TypeScript really does collapse those two into one here and the reason has to do with variance. This is the subject of a next post and it has no consequence for gen.

const gen: Gen<OptionLambda> = optionGen this line is where all the work we have done so far meets the implementation. The F type parameter of Gen is never inferred: we supply it by annotating it with OptionLambda.

There is a lot happening at the type level, let's decompose the types for Gen<OptionLambda>:

  • F is assigned to OptionLambda whose Target is still a placeholder (unknown)
  • Because F is now defined, Apply<F, any> is reduced to (OptionLambda & { Target: any })["type"] which is equal to Option<any>.
  • Similarly, because F is now defined Apply<F, A> (the return type) is reduced to (OptionLambda & { Target: A })["type"] which is equal to Option<A>. This works even though A is not known yet because unknown & A always reduces to A
  • After this reduction, the inner signature becomes <K extends Option<any>, A>(g: () => Generator<K, A, any>) => Option<A>
  • K and A are not yet defined, they remain abstract until we provide a function to build the generator

Now let's see what is happening when we call gen with program:

  • program is of type () => Generator<Some<number> | None<number> | Some<string>, readonly [number, string], any>, so K is inferred as Some<number> | None<number> | Some<string> and A as readonly [number, string]
  • the signature becomes (g: () => Generator<Some<number> | None<number> | Some<string>, readonly [number, string], any>) => Option<readonly [number, string]>

This typechecks because Some<number> | None<number> | Some<string> are Option<any>.

rinse and repeat for Result

We can try to apply the same methodology to Result but we have a problem: Apply takes a single Target and ResultLambda can't do anything with its error type.

What we need is a type lambda that representing (Target, Error) => Result<Target, Error> and for this we need an additional field inside our ResultLambda:

interface ResultLambda {
  readonly ResultLambda.Target: unknownTarget: unknown
  readonly ResultLambda.Error: unknownError: unknown
  readonly ResultLambda.type: Result<this["Target"], this["Error"]>type: type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<this["Target"], this["Error"]>
}

type 
type Apply2<F extends { readonly type: unknown; }, Error, Target> = (F & {
    readonly Error: Error;
    readonly Target: Target;
})["type"]
Apply2
<function (type parameter) F in type Apply2<F extends { readonly type: unknown; }, Error, Target>F extends { readonly type: unknowntype: unknown }, function (type parameter) Error in type Apply2<F extends { readonly type: unknown; }, Error, Target>Error, function (type parameter) Target in type Apply2<F extends { readonly type: unknown; }, Error, Target>Target> =
(function (type parameter) F in type Apply2<F extends { readonly type: unknown; }, Error, Target>F & { readonly type Error: ErrorError: function (type parameter) Error in type Apply2<F extends { readonly type: unknown; }, Error, Target>Error; readonly type Target: TargetTarget: function (type parameter) Target in type Apply2<F extends { readonly type: unknown; }, Error, Target>Target })["type"] interface interface Gen2<F extends { readonly type: unknown; }>Gen2<function (type parameter) F in Gen2<F extends { readonly type: unknown; }>F extends { readonly type: unknowntype: unknown }> { <function (type parameter) K in <K extends Apply2<F, any, any>, A>(g: () => Generator<K, A, any>): Apply2<F, K extends Apply2<F, infer E, any> ? E : never, A>K extends
type Apply2<F extends { readonly type: unknown; }, Error, Target> = (F & {
    readonly Error: Error;
    readonly Target: Target;
})["type"]
Apply2
<function (type parameter) F in Gen2<F extends { readonly type: unknown; }>F, any, any>, function (type parameter) A in <K extends Apply2<F, any, any>, A>(g: () => Generator<K, A, any>): Apply2<F, K extends Apply2<F, infer E, any> ? E : never, A>A>(
g: () => Generator<K, A, any>g: () => interface Generator<T = unknown, TReturn = any, TNext = any>Generator<function (type parameter) K in <K extends Apply2<F, any, any>, A>(g: () => Generator<K, A, any>): Apply2<F, K extends Apply2<F, infer E, any> ? E : never, A>K, function (type parameter) A in <K extends Apply2<F, any, any>, A>(g: () => Generator<K, A, any>): Apply2<F, K extends Apply2<F, infer E, any> ? E : never, A>A, any> ):
type Apply2<F extends { readonly type: unknown; }, Error, Target> = (F & {
    readonly Error: Error;
    readonly Target: Target;
})["type"]
Apply2
<
function (type parameter) F in Gen2<F extends { readonly type: unknown; }>F, function (type parameter) K in <K extends Apply2<F, any, any>, A>(g: () => Generator<K, A, any>): Apply2<F, K extends Apply2<F, infer E, any> ? E : never, A>K extends
type Apply2<F extends { readonly type: unknown; }, Error, Target> = (F & {
    readonly Error: Error;
    readonly Target: Target;
})["type"]
Apply2
<function (type parameter) F in Gen2<F extends { readonly type: unknown; }>F, infer function (type parameter) EE, any> ? function (type parameter) EE : never,
function (type parameter) A in <K extends Apply2<F, any, any>, A>(g: () => Generator<K, A, any>): Apply2<F, K extends Apply2<F, infer E, any> ? E : never, A>A > } const const gen: Gen2<ResultLambda>gen: interface Gen2<F extends { readonly type: unknown; }>Gen2<ResultLambda> = function resultGen<K extends Result<any, any>, A>(generator: () => Generator<K, A, any>): Result<A, ErrorOf<K>>resultGen function* program() {
function program(): Generator<Success<number, never> | Success<string, never> | Success<never, "boom"> | Failure<never, "boom">, readonly [number, string], any>
const const a: numbera = yield* function succeed<number>(value: number): Result<number, never>succeed(1) const const b: stringb = yield* function succeed<string>(value: string): Result<string, never>succeed("two") yield* function fail<"boom">(error: "boom"): Result<never, "boom">fail("boom" as type const = "boom"const) return [const a: numbera, const b: stringb] as type const = readonly [number, string]const } const r =
const gen: Gen2
<Success<number, never> | Success<string, never> | Success<never, "boom"> | Failure<never, "boom">, readonly [number, string]>(g: () => Generator<Success<number, never> | Success<string, never> | Success<never, "boom"> | Failure<never, "boom">, readonly [number, string], any>) => Result<readonly [number, string], "boom">
gen
(function program(): Generator<Success<number, never> | Success<string, never> | Success<never, "boom"> | Failure<never, "boom">, readonly [number, string], any>program)
const r: Result<readonly [number, string], "boom">

There is a bit more going on than in our example for Option but the idea is really similar, on const gen: Gen2<ResultLambda> = resultGen:

  • F of Gen2 is assigned to ResultLambda (both fields are still placeholders)
  • Apply2<F, any, any> is reduced to (ResultLambda & { readonly Target: any, readonly Error: any})["type"] which is equal to Result<any, any>.
  • The return type becomes Result<A, K extends Result<any, infer E> ? E : never>. The conditional cannot be reduced yet because K is still unknown.
  • Once K is inferred, the conditional extracts the error type from every Result yielded by the generator.

And then when we call it with gen(program):

  • program is of type () => Generator<Success<number, never> | Success<string, never> | Success<never, "boom"> | Failure<never, "boom">, readonly [number, string], any> so K is inferred as Success<number, never> | Success<string, never> | Success<never, "boom"> | Failure<never, "boom"> (which is Result<any, any>) and A as readonly [number, string]
  • the signature becomes (g: () => Generator<Success<number, never> | Success<string, never> | Success<never, "boom"> | Failure<never, "boom">, readonly [number, string], any>) => Result<readonly [number, string], "boom">

the arity challenge

This however did not bring us any closer to a common type definition between Option and Result: we had to create Apply, Apply2, Gen and Gen2. Furthermore if we needed another type parameter, let's say for the Effect type, we would need to introduce another type lambda, another Apply3 and Gen3.

The problem is that the number of type parameters is hardcoded into every type we write, Apply intersects a record with one field, Apply2 with two and we don't have a way to write a version that intersects "as many fields as needed" because we need to manually write the intersection by hand.

Effect's answer is to fix the number of slots at four, and let each type lambda use only the ones it needs:

interface TypeLambda {
    readonly TypeLambda.In: unknownIn: unknown
    readonly TypeLambda.Out2: unknownOut2: unknown
    readonly TypeLambda.Out1: unknownOut1: unknown
    readonly TypeLambda.Target: unknownTarget: unknown
}

interface ResultTypeLambda extends TypeLambda {
    readonly ResultTypeLambda.type: Result<this["Target"], this["Out1"]>type: type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<this["Target"], this["Out1"]>
}

interface OptionTypeLambda extends TypeLambda {
    readonly OptionTypeLambda.type: Option<this["Target"]>type: type Option<A> = Some<A> | None<A>Option<this["Target"]>
}

type 
type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target> = (F & {
    readonly In: In;
    readonly Out2: Out2;
    readonly Out1: Out1;
    readonly Target: Target;
})["type"]
Kind
<function (type parameter) F in type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target>F extends TypeLambda & { readonly type: unknowntype: unknown }, function (type parameter) In in type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target>In, function (type parameter) Out2 in type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target>Out2, function (type parameter) Out1 in type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target>Out1, function (type parameter) Target in type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target>Target> = (function (type parameter) F in type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target>F & {
readonly type In: InIn: function (type parameter) In in type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target>In readonly type Out2: Out2Out2: function (type parameter) Out2 in type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target>Out2 readonly type Out1: Out1Out1: function (type parameter) Out1 in type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target>Out1 readonly type Target: TargetTarget: function (type parameter) Target in type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target>Target })["type"] type A =
type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target> = (F & {
    readonly In: In;
    readonly Out2: Out2;
    readonly Out1: Out1;
    readonly Target: Target;
})["type"]
Kind
<ResultTypeLambda, never, never, string, number>
type A = Success<number, string> | Failure<number, string>
type B =
type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target> = (F & {
    readonly In: In;
    readonly Out2: Out2;
    readonly Out1: Out1;
    readonly Target: Target;
})["type"]
Kind
<OptionTypeLambda, never, never, never, number>
type B = Some<number> | None<number>

Kind here is the name that Effect is using for what we would call Apply4, ResultTypeLambda is using two channels: Target (left side) and Out1 for the error type (right side). OptionTypeLambda is only using Target.

We can now put the type signatures side by side. The one-slot version is renamed Gen1 here to distinguish it from Effect's Gen:

interface interface Gen1<F extends { readonly type: unknown; }>Gen1<function (type parameter) F in Gen1<F extends { readonly type: unknown; }>F extends { readonly type: unknowntype: unknown }> {
  <function (type parameter) K in <K extends Apply<F, any>, A>(g: () => Generator<K, A, any>): Apply<F, A>K extends 
type Apply<F extends { readonly type: unknown; }, Target> = (F & {
    readonly Target: Target;
})["type"]
Apply
<function (type parameter) F in Gen1<F extends { readonly type: unknown; }>F, any>, function (type parameter) A in <K extends Apply<F, any>, A>(g: () => Generator<K, A, any>): Apply<F, A>A>(
g: () => Generator<K, A, any>g: () => interface Generator<T = unknown, TReturn = any, TNext = any>Generator<function (type parameter) K in <K extends Apply<F, any>, A>(g: () => Generator<K, A, any>): Apply<F, A>K, function (type parameter) A in <K extends Apply<F, any>, A>(g: () => Generator<K, A, any>): Apply<F, A>A, any> ):
type Apply<F extends { readonly type: unknown; }, Target> = (F & {
    readonly Target: Target;
})["type"]
Apply
<function (type parameter) F in Gen1<F extends { readonly type: unknown; }>F, function (type parameter) A in <K extends Apply<F, any>, A>(g: () => Generator<K, A, any>): Apply<F, A>A>
} interface interface Gen2<F extends { readonly type: unknown; }>Gen2<function (type parameter) F in Gen2<F extends { readonly type: unknown; }>F extends { readonly type: unknowntype: unknown }> { <function (type parameter) K in <K extends Apply2<F, any, any>, A>(g: () => Generator<K, A, any>): Apply2<F, K extends Apply2<F, infer E, any> ? E : never, A>K extends
type Apply2<F extends { readonly type: unknown; }, Error, Target> = (F & {
    readonly Error: Error;
    readonly Target: Target;
})["type"]
Apply2
<function (type parameter) F in Gen2<F extends { readonly type: unknown; }>F, any, any>, function (type parameter) A in <K extends Apply2<F, any, any>, A>(g: () => Generator<K, A, any>): Apply2<F, K extends Apply2<F, infer E, any> ? E : never, A>A>(
g: () => Generator<K, A, any>g: () => interface Generator<T = unknown, TReturn = any, TNext = any>Generator<function (type parameter) K in <K extends Apply2<F, any, any>, A>(g: () => Generator<K, A, any>): Apply2<F, K extends Apply2<F, infer E, any> ? E : never, A>K, function (type parameter) A in <K extends Apply2<F, any, any>, A>(g: () => Generator<K, A, any>): Apply2<F, K extends Apply2<F, infer E, any> ? E : never, A>A, any> ):
type Apply2<F extends { readonly type: unknown; }, Error, Target> = (F & {
    readonly Error: Error;
    readonly Target: Target;
})["type"]
Apply2
<
function (type parameter) F in Gen2<F extends { readonly type: unknown; }>F, function (type parameter) K in <K extends Apply2<F, any, any>, A>(g: () => Generator<K, A, any>): Apply2<F, K extends Apply2<F, infer E, any> ? E : never, A>K extends
type Apply2<F extends { readonly type: unknown; }, Error, Target> = (F & {
    readonly Error: Error;
    readonly Target: Target;
})["type"]
Apply2
<function (type parameter) F in Gen2<F extends { readonly type: unknown; }>F, infer function (type parameter) EE, any> ? function (type parameter) EE : never,
function (type parameter) A in <K extends Apply2<F, any, any>, A>(g: () => Generator<K, A, any>): Apply2<F, K extends Apply2<F, infer E, any> ? E : never, A>A > } interface interface Gen<F extends TypeLambda & { readonly type: unknown; }>Gen<function (type parameter) F in Gen<F extends TypeLambda & { readonly type: unknown; }>F extends TypeLambda & { readonly type: unknowntype: unknown }> { <function (type parameter) K in <K extends Kind<F, any, any, any, any>, A>(g: () => Generator<K, A, any>): Kind<F, K extends Kind<F, infer R, any, any, any> ? R : never, K extends Kind<F, any, infer O, any, any> ? O : never, K extends Kind<F, any, any, infer E, any> ? E : never, A>K extends
type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target> = (F & {
    readonly In: In;
    readonly Out2: Out2;
    readonly Out1: Out1;
    readonly Target: Target;
})["type"]
Kind
<function (type parameter) F in Gen<F extends TypeLambda & { readonly type: unknown; }>F, any, any, any, any>, function (type parameter) A in <K extends Kind<F, any, any, any, any>, A>(g: () => Generator<K, A, any>): Kind<F, K extends Kind<F, infer R, any, any, any> ? R : never, K extends Kind<F, any, infer O, any, any> ? O : never, K extends Kind<F, any, any, infer E, any> ? E : never, A>A>(
g: () => Generator<K, A, any>g: () => interface Generator<T = unknown, TReturn = any, TNext = any>Generator<function (type parameter) K in <K extends Kind<F, any, any, any, any>, A>(g: () => Generator<K, A, any>): Kind<F, K extends Kind<F, infer R, any, any, any> ? R : never, K extends Kind<F, any, infer O, any, any> ? O : never, K extends Kind<F, any, any, infer E, any> ? E : never, A>K, function (type parameter) A in <K extends Kind<F, any, any, any, any>, A>(g: () => Generator<K, A, any>): Kind<F, K extends Kind<F, infer R, any, any, any> ? R : never, K extends Kind<F, any, infer O, any, any> ? O : never, K extends Kind<F, any, any, infer E, any> ? E : never, A>A, any> ):
type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target> = (F & {
    readonly In: In;
    readonly Out2: Out2;
    readonly Out1: Out1;
    readonly Target: Target;
})["type"]
Kind
<
function (type parameter) F in Gen<F extends TypeLambda & { readonly type: unknown; }>F, function (type parameter) K in <K extends Kind<F, any, any, any, any>, A>(g: () => Generator<K, A, any>): Kind<F, K extends Kind<F, infer R, any, any, any> ? R : never, K extends Kind<F, any, infer O, any, any> ? O : never, K extends Kind<F, any, any, infer E, any> ? E : never, A>K extends
type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target> = (F & {
    readonly In: In;
    readonly Out2: Out2;
    readonly Out1: Out1;
    readonly Target: Target;
})["type"]
Kind
<function (type parameter) F in Gen<F extends TypeLambda & { readonly type: unknown; }>F, infer function (type parameter) RR, any, any, any> ? function (type parameter) RR : never,
function (type parameter) K in <K extends Kind<F, any, any, any, any>, A>(g: () => Generator<K, A, any>): Kind<F, K extends Kind<F, infer R, any, any, any> ? R : never, K extends Kind<F, any, infer O, any, any> ? O : never, K extends Kind<F, any, any, infer E, any> ? E : never, A>K extends
type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target> = (F & {
    readonly In: In;
    readonly Out2: Out2;
    readonly Out1: Out1;
    readonly Target: Target;
})["type"]
Kind
<function (type parameter) F in Gen<F extends TypeLambda & { readonly type: unknown; }>F, any, infer function (type parameter) OO, any, any> ? function (type parameter) OO : never,
function (type parameter) K in <K extends Kind<F, any, any, any, any>, A>(g: () => Generator<K, A, any>): Kind<F, K extends Kind<F, infer R, any, any, any> ? R : never, K extends Kind<F, any, infer O, any, any> ? O : never, K extends Kind<F, any, any, infer E, any> ? E : never, A>K extends
type Kind<F extends TypeLambda & { readonly type: unknown; }, In, Out2, Out1, Target> = (F & {
    readonly In: In;
    readonly Out2: Out2;
    readonly Out1: Out1;
    readonly Target: Target;
})["type"]
Kind
<function (type parameter) F in Gen<F extends TypeLambda & { readonly type: unknown; }>F, any, any, infer function (type parameter) EE, any> ? function (type parameter) EE : never,
function (type parameter) A in <K extends Kind<F, any, any, any, any>, A>(g: () => Generator<K, A, any>): Kind<F, K extends Kind<F, infer R, any, any, any> ? R : never, K extends Kind<F, any, infer O, any, any> ? O : never, K extends Kind<F, any, any, infer E, any> ? E : never, A>A > } const const newResultGen: Gen<ResultTypeLambda>newResultGen: interface Gen<F extends TypeLambda & { readonly type: unknown; }>Gen<ResultTypeLambda> = function resultGen<K extends Result<any, any>, A>(generator: () => Generator<K, A, any>): Result<A, ErrorOf<K>>resultGen const const newOptionGen: Gen<OptionTypeLambda>newOptionGen: interface Gen<F extends TypeLambda & { readonly type: unknown; }>Gen<OptionTypeLambda> = function optionGen<K extends Option<any>, A>(generator: () => Generator<K, A, any>): Option<A>optionGen

Calling both implementations shows that the shared signature preserves the inference we had before:


function* function resultProgram(): Generator<Success<number, never> | Success<string, never> | Success<never, "boom"> | Failure<never, "boom">, readonly [number, string], any>resultProgram() {
  const const a: numbera = yield* function succeed<number>(value: number): Result<number, never>succeed(1)
  const const b: stringb = yield* function succeed<string>(value: string): Result<string, never>succeed("two")
  yield* function fail<"boom">(error: "boom"): Result<never, "boom">fail("boom" as type const = "boom"const)
  return [const a: numbera, const b: stringb] as type const = readonly [number, string]const
}
const r = 
const newResultGen: Gen
<Success<number, never> | Success<string, never> | Success<never, "boom"> | Failure<never, "boom">, readonly [number, string]>(g: () => Generator<Success<number, never> | Success<string, never> | Success<never, "boom"> | Failure<never, "boom">, readonly [number, string], any>) => Result<readonly [number, string], "boom">
newResultGen
(function resultProgram(): Generator<Success<number, never> | Success<string, never> | Success<never, "boom"> | Failure<never, "boom">, readonly [number, string], any>resultProgram)
const r: Result<readonly [number, string], "boom">
function* function optionProgram(): Generator<Some<number> | None<number> | Some<string>, readonly [number, string], any>optionProgram() { const const a: numbera = yield* function some<number>(value: number): Option<number>some(1) const const b: stringb = yield* function some<string>(value: string): Option<string>some("two") return [const a: numbera, const b: stringb] as type const = readonly [number, string]const } const s =
const newOptionGen: Gen
<Some<number> | None<number> | Some<string>, readonly [number, string]>(g: () => Generator<Some<number> | None<number> | Some<string>, readonly [number, string], any>) => Option<readonly [number, string]>
newOptionGen
(function optionProgram(): Generator<Some<number> | None<number> | Some<string>, readonly [number, string], any>optionProgram)
const s: Option<readonly [number, string]>

conclusion

We managed to reproduce and understand how Effect managed to create a common type between the gen function of Result and Option. The key learning here is that TypeLambda is giving us a type representation of a type constructor, allowing us to work around a limitation from typescript preventing us from using a type constructor as a type parameter.

By fixing the number of slot to four, we can let each implementation of TypeLambda use only what it needs.

This was a lot of type gymnastic and I'm still unsure about the value is brings for the added complexity but this is a cool trick.