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)
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)
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.
type type ResultValue<T> = T extends Result<infer A, any> ? A : neverResultValue<function (type parameter) T in type ResultValue<T>T> = function (type parameter) T in type ResultValue<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, ResultValue<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 ResultValue<T> = T extends Result<infer A, any> ? A : neverResultValue<function (type parameter) T in ResultIterator<T extends Result<any, any>>T>>;
}
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.iteratorA 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.iteratorA 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>>;
}
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>;
type type OptionValue<T extends Option<any>> = T extends Option<infer A> ? A : neverOptionValue<function (type parameter) T in type OptionValue<T extends Option<any>>T extends type Option<A> = Some<A> | None<A>Option<any>> = function (type parameter) T in type OptionValue<T extends Option<any>>T extends type Option<A> = Some<A> | None<A>Option<infer function (type parameter) AA> ? function (type parameter) AA : never;
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, OptionValue<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 OptionValue<T extends Option<any>> = T extends Option<infer A> ? A : neverOptionValue<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.iteratorA 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.iteratorA 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>;
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 = false;
constructor(private readonly SingleShotIterator<T, A>.self: Tself: function (type parameter) T in SingleShotIterator<T, A>T) {}
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.iteratorA 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);
}
}
function function succeed<A>(value: A): Result<A>succeed<function (type parameter) A in succeed<A>(value: A): Result<A>A>(value: Avalue: function (type parameter) A in succeed<A>(value: A): Result<A>A): type Result<A, E = never> = Success<A, E> | Failure<A, E>Result<function (type parameter) A in succeed<A>(value: A): Result<A>A> {
const const self: Success<A, never>self: interface Success<A, E>Success<function (type parameter) A in succeed<A>(value: A): Result<A>A, never> = {
Success<A, E>._tag: "Success"_tag: "Success",
Success<A, never>.value: Avalue,
[var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iteratorA 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>A>, function (type parameter) A in succeed<A>(value: A): Result<A>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<A, E>._tag: "Failure"_tag: "Failure",
Failure<never, E>.error: Eerror,
[var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iteratorA 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;
}
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,
[var Symbol: SymbolConstructorSymbol.SymbolConstructor.iterator: typeof Symbol.iteratorA 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.iteratorA 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;
}
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 resultGen<K extends Result<any, any>, A>(generator: () => Generator<K, A, any>): Result<A, ErrorOf<K>>resultGen<function (type parameter) K in resultGen<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 resultGen<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 resultGen<K extends Result<any, any>, A>(generator: () => Generator<K, A, any>): Result<A, ErrorOf<K>>K, function (type parameter) A in resultGen<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 resultGen<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 resultGen<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 function optionGen<K extends Option<any>, A>(generator: () => Generator<K, A, any>): Option<A>optionGen<function (type parameter) K in optionGen<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 optionGen<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 optionGen<K extends Option<any>, A>(generator: () => Generator<K, A, any>): Option<A>K, function (type parameter) A in optionGen<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 optionGen<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);
}
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>>
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>
// 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>
Let's take some time to process what we are doing here:
Fhas one constraint: it must contain a field calledtype. This is required for the final["type"]access to workTargetis a placeholder, it only exists sotypecan exist.typeis a type, but is in reality waiting forTargetto 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() { 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)
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>:
Fis assigned toOptionLambdawhoseTargetis still a placeholder (unknown)- Because
Fis now defined,Apply<F, any>is reduced to(OptionLambda & { Target: any })["type"]which is equal toOption<any>. - Similarly, because
Fis now definedApply<F, A>(the return type) is reduced to(OptionLambda & { Target: A })["type"]which is equal toOption<A>. This works even thoughAis not known yet becauseunknown & Aalways reduces toA - After this reduction, the inner signature becomes
<K extends Option<any>, A>(g: () => Generator<K, A, any>) => Option<A> KandAare 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:
programis of type() => Generator<Some<number> | None<number> | Some<string>, readonly [number, string], any>, soKis inferred asSome<number> | None<number> | Some<string>andAasreadonly [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() { 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)
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:
FofGen2is assigned toResultLambda(both fields are still placeholders)Apply2<F, any, any>is reduced to(ResultLambda & { readonly Target: any, readonly Error: any})["type"]which is equal toResult<any, any>.- The return type becomes
Result<A, K extends Result<any, infer E> ? E : never>. The conditional cannot be reduced yet becauseKis still unknown. - Once
Kis inferred, the conditional extracts the error type from everyResultyielded by the generator.
And then when we call it with gen(program):
programis of type() => Generator<Success<number, never> | Success<string, never> | Success<never, "boom"> | Failure<never, "boom">, readonly [number, string], any>soKis inferred asSuccess<number, never> | Success<string, never> | Success<never, "boom"> | Failure<never, "boom">(which isResult<any, any>) andAasreadonly [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 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>
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)
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)
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.