Skip to content

Instantly share code, notes, and snippets.

@isaacharrisholt
Created July 2, 2026 14:34
Show Gist options
  • Select an option

  • Save isaacharrisholt/997a17a03a3080cac6bb187e22406062 to your computer and use it in GitHub Desktop.

Select an option

Save isaacharrisholt/997a17a03a3080cac6bb187e22406062 to your computer and use it in GitHub Desktop.
Serialisable TypeScript results, inspired by better-result
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/** Discriminated union representing success or failure */
export type Result<T, E> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: E };
/** Infer success type from Result */
export type InferOk<R> = R extends { ok: true; value: infer T } ? T : never;
/** Infer error type from Result */
export type InferErr<R> = R extends { ok: false; error: infer E } ? E : never;
/** Error marker for generator yields */
export type YieldErr<E> = { readonly ok: false; readonly error: E };
/** Extracts error type from yield union in Result.gen */
type InferYieldErr<Y> = Y extends { ok: false; error: infer E } ? E : never;
/** Constraint for any Result */
type AnyResult = Result<unknown, unknown>;
/** Extract Ok value type from Promise<Result<T, E>> */
type AwaitedOk<T> = T extends Promise<Result<infer V, unknown>> ? V : never;
/** Extract Err type from Promise<Result<T, E>> */
type AwaitedErr<T> = T extends Promise<Result<unknown, infer E>> ? E : never;
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/*
* Creates data-first/data-last dual function.
*
* @param arity Number of args for data-first form.
* @param body Implementation function.
* @returns Function supporting both calling conventions.
*
* @example
* const add: {
* (a: number, b: number): number;
* (b: number): (a: number) => number;
* } = dual(2, (a: number, b: number) => a + b);
*
* add(1, 2); // 3 (data-first)
* add(2)(1); // 3 (data-last)
*/
export function dual<
DataLast extends (...args: Array<any>) => any,
DataFirst extends (...args: Array<any>) => any,
>(arity: Parameters<DataFirst>['length'], body: DataFirst): DataLast & DataFirst {
if (arity === 2) {
return ((...args: Array<any>) => {
if (args.length >= 2) {
return body(args[0], args[1]);
}
return (self: any) => body(self, args[0]);
}) as DataLast & DataFirst;
}
if (arity === 3) {
return ((...args: Array<any>) => {
if (args.length >= 3) {
return body(args[0], args[1], args[2]);
}
return (self: any) => body(self, args[0], args[1]);
}) as DataLast & DataFirst;
}
if (arity === 4) {
return ((...args: Array<any>) => {
if (args.length >= 4) {
return body(args[0], args[1], args[2], args[3]);
}
return (self: any) => body(self, args[0], args[1], args[2]);
}) as DataLast & DataFirst;
}
return ((...args: Array<any>) => {
if (args.length >= arity) {
return body(...args);
}
return (self: any) => body(self, ...args);
}) as DataLast & DataFirst;
}
/** Executes fn, panics if it throws */
const tryOrPanic = <T>(fn: () => T, message: string): T => {
try {
return fn();
} catch (cause) {
throw panic(message, cause);
}
};
/** Async version of tryOrPanic */
const tryOrPanicAsync = async <T>(fn: () => Promise<T>, message: string): Promise<T> => {
try {
return await fn();
} catch (cause) {
throw panic(message, cause);
}
};
/** Validates that a value is a Result. Throws with helpful message if not. */
function assertIsResult(value: unknown): asserts value is Result<unknown, unknown> {
if (
value !== null &&
typeof value === 'object' &&
'ok' in value &&
typeof value.ok === 'boolean'
) {
return;
}
throw panic(
'Result.gen body must return Result.ok() or Result.err(), got: ' +
(value === null
? 'null'
: typeof value === 'object'
? JSON.stringify(value)
: String(value)),
);
}
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
/** Serialise cause for JSON output */
const serialiseCause = (cause: unknown): unknown => {
if (cause instanceof Error) {
return { name: cause.name, message: cause.message, stack: cause.stack };
}
return cause;
};
/** Any tagged error (for generic constraints) */
type AnyTaggedError = Error & { readonly _tag: string };
/** Type guard for any tagged error */
const isAnyTaggedError = (value: unknown): value is AnyTaggedError => {
return value instanceof Error && '_tag' in value && typeof value._tag === 'string';
};
/**
* Factory for tagged error classes.
*
* @example
* class NotFoundError extends TaggedError("NotFoundError")<{
* id: string;
* message: string;
* }> {}
*
* const err = new NotFoundError({ id: "123", message: "Not found: 123" });
* err._tag // "NotFoundError"
* err.id // "123"
* err.message // "Not found: 123"
*
* // Check if any tagged error
* TaggedError.is(err) // true
*/
export const TaggedError: {
<Tag extends string>(
tag: Tag,
): <
Props extends Record<string, unknown> = Record<string, never>,
>() => TaggedErrorClass<Tag, Props>;
/** Type guard for any TaggedError instance */
is(value: unknown): value is AnyTaggedError;
} = Object.assign(
<Tag extends string>(tag: Tag) =>
<Props extends Record<string, unknown> = Record<string, never>>(): TaggedErrorClass<
Tag,
Props
> => {
class Base extends Error {
readonly _tag: Tag = tag;
/** Type guard for this error class */
static is(value: unknown): value is Base {
return value instanceof Base;
}
constructor(args?: Props) {
const message =
args && 'message' in args && typeof args.message === 'string'
? args.message
: undefined;
const cause = args && 'cause' in args ? args.cause : undefined;
super(message, cause !== undefined ? { cause } : undefined);
if (args) {
Object.assign(this, args);
}
Object.setPrototypeOf(this, new.target.prototype);
this.name = tag;
if (cause instanceof Error && cause.stack) {
const indented = cause.stack.replace(/\n/g, '\n ');
this.stack = `${this.stack}\nCaused by: ${indented}`;
}
}
toJSON(): object {
return {
...this,
_tag: this._tag,
name: this.name,
message: this.message,
cause: serialiseCause(this.cause),
stack: this.stack,
};
}
}
// SAFETY: Cast needed for factory pattern - Props are assigned via Object.assign
return Base as unknown as TaggedErrorClass<Tag, Props>;
},
{ is: isAnyTaggedError },
);
/** Instance type produced by TaggedError factory */
export type TaggedErrorInstance<Tag extends string, Props> = Error & {
readonly _tag: Tag;
toJSON(): object;
} & Readonly<Props>;
/** Class type produced by TaggedError factory */
export type TaggedErrorClass<Tag extends string, Props> = {
new (
...args: keyof Props extends never ? [args?: Record<string, never>] : [args: Props]
): TaggedErrorInstance<Tag, Props>;
/** Type guard for this error class */
is(value: unknown): value is TaggedErrorInstance<Tag, Props>;
};
/** Handler map for exhaustive matching */
export type MatchHandlers<E extends AnyTaggedError, R> = {
[K in E['_tag']]: (err: Extract<E, { _tag: K }>) => R;
};
/**
* Exhaustive pattern match on tagged error union.
*
* @example
* // Data-first
* matchError(err, {
* NotFoundError: (e) => `Missing: ${e.id}`,
* ValidationError: (e) => `Invalid: ${e.field}`,
* });
*
* // Data-last (pipeable)
* pipe(err, matchError({
* NotFoundError: (e) => `Missing: ${e.id}`,
* ValidationError: (e) => `Invalid: ${e.field}`,
* }));
*/
export const matchError: {
<E extends AnyTaggedError, R>(err: E, handlers: MatchHandlers<E, R>): R;
<E extends AnyTaggedError, R>(handlers: MatchHandlers<E, R>): (err: E) => R;
} = dual(2, <E extends AnyTaggedError, R>(err: E, handlers: MatchHandlers<E, R>): R => {
const handler = handlers[err._tag as E['_tag']];
// SAFETY: handler exists if handlers satisfies MatchHandlers<E, R>
return handler(err as Extract<E, { _tag: (typeof err)['_tag'] }>);
});
/**
* Partial pattern match with fallback for unhandled tags.
*
* @example
* matchErrorPartial(err, {
* NotFoundError: (e) => `Missing: ${e.id}`,
* }, (e) => `Unknown: ${e.message}`);
*/
export const matchErrorPartial: {
<E extends AnyTaggedError, R>(
err: E,
handlers: Partial<MatchHandlers<E, R>>,
fallback: (e: E) => R,
): R;
<E extends AnyTaggedError, R>(
handlers: Partial<MatchHandlers<E, R>>,
fallback: (e: E) => R,
): (err: E) => R;
} = dual(
3,
<E extends AnyTaggedError, R>(
err: E,
handlers: Partial<MatchHandlers<E, R>>,
fallback: (e: E) => R,
): R => {
const handler = handlers[err._tag as E['_tag']];
if (handler) {
// SAFETY: handler exists and matches the tag
return handler(err as Extract<E, { _tag: (typeof err)['_tag'] }>);
}
return fallback(err);
},
);
/**
* Type guard for tagged error instances.
*
* @example
* if (isTaggedError(value)) { value._tag }
*/
export const isTaggedError = isAnyTaggedError;
/**
* Wraps exceptions caught by Result.try/tryPromise.
* Custom constructor derives message from cause.
*/
export class UnhandledException extends TaggedError('UnhandledException')<{
message: string;
cause: unknown;
}>() {
constructor(args: { cause: unknown }) {
const message =
args.cause instanceof Error
? `Unhandled exception: ${args.cause.message}`
: `Unhandled exception: ${String(args.cause)}`;
super({ message, cause: args.cause });
}
}
/**
* Unrecoverable error - user code threw inside Result operations.
*
* @example
* // Panic in generator cleanup:
* Result.gen(function* () {
* try {
* yield* Result.run(Result.err("expected error"));
* } finally {
* throw new Error("cleanup failed"); // Panic!
* }
* });
*
* // Panic in combinator:
* Result.map(Result.ok(1), () => { throw new Error("oops"); }); // Panic!
*/
export class Panic extends TaggedError('Panic')<{
message: string;
cause?: unknown;
}>() {}
/**
* Type guard for Panic instances.
*
* @example
* if (isPanic(value)) { value.cause }
*/
export const isPanic = (value: unknown): value is Panic => {
return value instanceof Panic;
};
/**
* Throw an unrecoverable Panic.
*
* @example
* panic("something went wrong", cause);
*/
export const panic = (message: string, cause?: unknown): never => {
throw new Panic({ message, cause });
};
export class Unreachable extends TaggedError('Unreachable')<{
message: string;
}>() {}
/**
* Throw an unreachable error.
*
* @example
* unreachable("this should never happen");
*/
export const unreachable = (message: string): never => {
throw new Unreachable({ message });
};
// ---------------------------------------------------------------------------
// Constructors
// ---------------------------------------------------------------------------
const ok = <T, E = never>(value: T): Result<T, E> => ({ ok: true, value });
const err = <T = never, E = unknown>(error: E): Result<T, E> => ({ ok: false, error });
// ---------------------------------------------------------------------------
// Transformers
// ---------------------------------------------------------------------------
const map: {
<A, B, E>(result: Result<A, E>, fn: (a: A) => B): Result<B, E>;
<A, B>(fn: (a: A) => B): <E>(result: Result<A, E>) => Result<B, E>;
} = dual(2, <A, B, E>(result: Result<A, E>, fn: (a: A) => B): Result<B, E> => {
if (!result.ok) return result;
return tryOrPanic(() => ok(fn(result.value)), 'map callback threw');
});
const mapError: {
<A, E, E2>(result: Result<A, E>, fn: (e: E) => E2): Result<A, E2>;
<E, E2>(fn: (e: E) => E2): <A>(result: Result<A, E>) => Result<A, E2>;
} = dual(2, <A, E, E2>(result: Result<A, E>, fn: (e: E) => E2): Result<A, E2> => {
if (result.ok) return result;
return tryOrPanic(() => err(fn(result.error)), 'mapError callback threw');
});
const andThen: {
<A, B, E, E2>(result: Result<A, E>, fn: (a: A) => Result<B, E2>): Result<B, E | E2>;
<A, B, E2>(fn: (a: A) => Result<B, E2>): <E>(result: Result<A, E>) => Result<B, E | E2>;
} = dual(
2,
<A, B, E, E2>(result: Result<A, E>, fn: (a: A) => Result<B, E2>): Result<B, E | E2> => {
if (!result.ok) return result;
return tryOrPanic(() => fn(result.value), 'andThen callback threw');
},
);
const andThenAsync: {
<A, B, E, E2>(
result: Result<A, E>,
fn: (a: A) => Promise<Result<B, E2>>,
): Promise<Result<B, E | E2>>;
<A, B, E2>(
fn: (a: A) => Promise<Result<B, E2>>,
): <E>(result: Result<A, E>) => Promise<Result<B, E | E2>>;
} = dual(
2,
async <A, B, E, E2>(
result: Result<A, E>,
fn: (a: A) => Promise<Result<B, E2>>,
): Promise<Result<B, E | E2>> => {
if (!result.ok) return result;
return tryOrPanicAsync(() => fn(result.value), 'andThenAsync callback threw');
},
);
// ---------------------------------------------------------------------------
// Pattern Matching
// ---------------------------------------------------------------------------
const match: {
<A, E, T>(result: Result<A, E>, handlers: { ok: (a: A) => T; err: (e: E) => T }): T;
<A, E, T>(handlers: { ok: (a: A) => T; err: (e: E) => T }): (result: Result<A, E>) => T;
} = dual(
2,
<A, E, T>(result: Result<A, E>, handlers: { ok: (a: A) => T; err: (e: E) => T }): T => {
if (result.ok) {
return tryOrPanic(() => handlers.ok(result.value), 'match ok handler threw');
}
return tryOrPanic(() => handlers.err(result.error), 'match err handler threw');
},
);
// ---------------------------------------------------------------------------
// Side Effects
// ---------------------------------------------------------------------------
const tap: {
<A, E>(result: Result<A, E>, fn: (a: A) => void): Result<A, E>;
<A>(fn: (a: A) => void): <E>(result: Result<A, E>) => Result<A, E>;
} = dual(2, <A, E>(result: Result<A, E>, fn: (a: A) => void): Result<A, E> => {
if (!result.ok) return result;
return tryOrPanic(() => {
fn(result.value);
return result;
}, 'tap callback threw');
});
const tapAsync: {
<A, E>(result: Result<A, E>, fn: (a: A) => Promise<void>): Promise<Result<A, E>>;
<A>(fn: (a: A) => Promise<void>): <E>(result: Result<A, E>) => Promise<Result<A, E>>;
} = dual(
2,
async <A, E>(
result: Result<A, E>,
fn: (a: A) => Promise<void>,
): Promise<Result<A, E>> => {
if (!result.ok) return result;
return tryOrPanicAsync(async () => {
await fn(result.value);
return result;
}, 'tapAsync callback threw');
},
);
// ---------------------------------------------------------------------------
// Unwrapping
// ---------------------------------------------------------------------------
const unwrap = <A, E>(result: Result<A, E>, message?: string): A => {
if (result.ok) return result.value;
throw panic(message ?? `Unwrap called on Err: ${String(result.error)}`, result.error);
};
const unwrapOr: {
<A, E, B>(result: Result<A, E>, fallback: B): A | B;
<B>(fallback: B): <A, E>(result: Result<A, E>) => A | B;
} = dual(2, <A, E, B>(result: Result<A, E>, fallback: B): A | B => {
return result.ok ? result.value : fallback;
});
// ---------------------------------------------------------------------------
// Exception Handling
// ---------------------------------------------------------------------------
const tryFn: {
<A>(
thunk: () => A,
config?: { retry?: { times: number } },
): Result<A, UnhandledException>;
<A, E>(
options: { try: () => A; catch: (cause: unknown) => E },
config?: { retry?: { times: number } },
): Result<A, E>;
} = <A, E>(
options: (() => A) | { try: () => A; catch: (cause: unknown) => E },
config?: { retry?: { times: number } },
): Result<A, E | UnhandledException> => {
const execute = (): Result<A, E | UnhandledException> => {
if (typeof options === 'function') {
try {
return ok(options());
} catch (cause) {
return err(new UnhandledException({ cause }));
}
}
try {
return ok(options.try());
} catch (originalCause) {
try {
return err(options.catch(originalCause));
} catch (catchHandlerError) {
throw panic('Result.try catch handler threw', catchHandlerError);
}
}
};
const times = config?.retry?.times ?? 0;
let result = execute();
for (let retry = 0; retry < times && !result.ok; retry++) {
result = execute();
}
return result;
};
type RetryConfig<E = unknown> = {
retry?: {
times: number;
delayMs: number;
backoff: 'linear' | 'constant' | 'exponential';
/** Predicate to determine if an error should trigger a retry. Defaults to always retry. */
shouldRetry?: (error: E) => boolean;
};
};
const tryPromise: {
<A>(
thunk: () => Promise<A>,
config?: RetryConfig<UnhandledException>,
): Promise<Result<A, UnhandledException>>;
<A, E>(
options: {
try: () => Promise<A>;
catch: (cause: unknown) => E | Promise<E>;
},
config?: RetryConfig<E>,
): Promise<Result<A, E>>;
} = async <A, E>(
options:
| (() => Promise<A>)
| { try: () => Promise<A>; catch: (cause: unknown) => E | Promise<E> },
config?: RetryConfig<E | UnhandledException>,
): Promise<Result<A, E | UnhandledException>> => {
const execute = async (): Promise<Result<A, E | UnhandledException>> => {
if (typeof options === 'function') {
try {
return ok(await options());
} catch (cause) {
return err(new UnhandledException({ cause }));
}
}
try {
return ok(await options.try());
} catch (originalCause) {
try {
return err(await options.catch(originalCause));
} catch (catchHandlerError) {
throw panic('Result.tryPromise catch handler threw', catchHandlerError);
}
}
};
const retry = config?.retry;
if (!retry) {
return execute();
}
const getDelay = (retryAttempt: number): number => {
switch (retry.backoff) {
case 'constant':
return retry.delayMs;
case 'linear':
return retry.delayMs * (retryAttempt + 1);
case 'exponential':
return retry.delayMs * 2 ** retryAttempt;
}
};
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
let result = await execute();
const shouldRetryFn = retry.shouldRetry ?? (() => true);
for (let attempt = 0; attempt < retry.times; attempt++) {
if (result.ok) break;
const error = result.error;
const shouldContinue = tryOrPanic(
() => shouldRetryFn(error),
'shouldRetry predicate threw',
);
if (!shouldContinue) break;
await sleep(getDelay(attempt));
result = await execute();
}
return result;
};
// ---------------------------------------------------------------------------
// Generator Composition
// ---------------------------------------------------------------------------
/** Wrap sync Result for yield* in generators */
function* run<T, E>(result: Result<T, E>): Generator<YieldErr<E>, T, unknown> {
if (result.ok) {
return result.value;
}
yield result;
throw panic(
'Unreachable: Err yielded in Result.gen but generator continued',
result.error,
);
}
/** Wrap Promise<Result> for yield* in async generators */
async function* resultAwait<T, E>(
promise: Promise<Result<T, E>>,
): AsyncGenerator<YieldErr<E>, T, unknown> {
const result = await promise;
return yield* run(result);
}
/** Wrap array of Promise<Result> for parallel execution, like Promise.all */
async function* resultAwaitAll<
const T extends readonly Promise<Result<unknown, unknown>>[],
>(
promises: T,
): AsyncGenerator<
YieldErr<AwaitedErr<T[number]>>,
{ -readonly [K in keyof T]: AwaitedOk<T[K]> },
unknown
> {
const results = await Promise.all(promises);
// Check for first error
for (const result of results) {
if (!result.ok) {
return yield* run(result as Result<never, AwaitedErr<T[number]>>);
}
}
// All succeeded - extract values preserving tuple structure
return results.map((r) => (r as { ok: true; value: unknown }).value) as {
-readonly [K in keyof T]: AwaitedOk<T[K]>;
};
}
const gen: {
<Yield extends YieldErr<unknown>, R extends AnyResult>(
body: () => Generator<Yield, R, unknown>,
): Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>;
<Yield extends YieldErr<unknown>, R extends AnyResult, This>(
body: (this: This) => Generator<Yield, R, unknown>,
thisArg: This,
): Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>;
<Yield extends YieldErr<unknown>, R extends AnyResult>(
body: () => AsyncGenerator<Yield, R, unknown>,
): Promise<Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>>;
<Yield extends YieldErr<unknown>, R extends AnyResult, This>(
body: (this: This) => AsyncGenerator<Yield, R, unknown>,
thisArg: This,
): Promise<Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>>;
} = (<Yield extends YieldErr<unknown>, R extends AnyResult, This>(
body:
| (() => Generator<Yield, R, unknown>)
| (() => AsyncGenerator<Yield, R, unknown>)
| ((this: This) => Generator<Yield, R, unknown>)
| ((this: This) => AsyncGenerator<Yield, R, unknown>),
thisArg?: This,
):
| Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>
| Promise<Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>> => {
// SAFETY: body.call binds thisArg; cast needed due to union of function signatures
const iterator = (body as (this: This) => Generator<Yield, R, unknown>).call(
thisArg as This,
);
// Detect async generator via Symbol.asyncIterator
if (Symbol.asyncIterator in iterator) {
return (async () => {
// SAFETY: Async check above guarantees this is an async generator
const asyncIter = iterator as unknown as AsyncGenerator<Yield, R, unknown>;
let state: IteratorResult<Yield, R>;
try {
state = await asyncIter.next();
} catch (cause) {
// Generator body threw before yielding (user code error or cleanup on success path)
throw panic('generator body threw', cause);
}
assertIsResult(state.value);
if (!state.done) {
// Generator yielded an error result. Attempt cleanup via .return() but
// tolerate failures: JS async generators propagating .return() through
// yield* can trigger user code (e.g. destructuring) that throws. Since
// we already have the error result, cleanup failure is non-fatal.
try {
await asyncIter.return?.(undefined as unknown as R);
} catch {
// Cleanup failed, but we have the error result - continue
}
}
return state.value as Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>;
})();
}
// Sync generator
// SAFETY: If not async, must be sync generator
const syncIter = iterator as Generator<Yield, R, unknown>;
let state: IteratorResult<Yield, R>;
try {
state = syncIter.next();
} catch (cause) {
// Generator body threw before yielding (user code error or cleanup on success path)
throw panic('generator body threw', cause);
}
assertIsResult(state.value);
if (!state.done) {
// Generator yielded an error result. Attempt cleanup via .return() but
// tolerate failures: sync generators propagating .return() through yield*
// can trigger user code (e.g. destructuring) that throws. Since we already
// have the error result, cleanup failure is non-fatal.
try {
syncIter.return?.(undefined as unknown as R);
} catch {
// Cleanup failed, but we have the error result - continue
}
}
return state.value as Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>;
}) as {
<Yield extends YieldErr<unknown>, R extends AnyResult>(
body: () => Generator<Yield, R, unknown>,
): Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>;
<Yield extends YieldErr<unknown>, R extends AnyResult, This>(
body: (this: This) => Generator<Yield, R, unknown>,
thisArg: This,
): Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>;
<Yield extends YieldErr<unknown>, R extends AnyResult>(
body: () => AsyncGenerator<Yield, R, unknown>,
): Promise<Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>>;
<Yield extends YieldErr<unknown>, R extends AnyResult, This>(
body: (this: This) => AsyncGenerator<Yield, R, unknown>,
thisArg: This,
): Promise<Result<InferOk<R>, InferYieldErr<Yield> | InferErr<R>>>;
};
// ---------------------------------------------------------------------------
// Utilities
// ---------------------------------------------------------------------------
const partition = <T, E>(results: readonly Result<T, E>[]): [T[], E[]] => {
const oks: T[] = [];
const errs: E[] = [];
for (const r of results) {
if (r.ok) {
oks.push(r.value);
} else {
errs.push(r.error);
}
}
return [oks, errs];
};
const flatten = <T, E, E2>(result: Result<Result<T, E>, E2>): Result<T, E | E2> => {
if (result.ok) return result.value;
return result;
};
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
/**
* Utilities for creating and handling Result types.
*
* @example
* const result = Result.try(() => JSON.parse(str));
* const value = Result.unwrapOr(Result.map(result, x => x.id), "default");
*/
export const Result = {
/** Creates successful result. */
ok,
/** Creates error result. */
err,
/** Executes sync function, wraps result/error in Result. */
try: tryFn,
/** Executes async function, wraps result/error in Result with retry support. */
tryPromise,
/** Transforms success value, passes error through. */
map,
/** Transforms error value, passes success through. */
mapError,
/** Chains Result-returning function on success. */
andThen,
/** Chains async Result-returning function on success. */
andThenAsync,
/** Pattern matches on Result. */
match,
/** Runs side effect on success value, returns original result. */
tap,
/** Runs async side effect on success value, returns original result. */
tapAsync,
/** Extracts value or throws. */
unwrap,
/** Extracts value or returns fallback. */
unwrapOr,
/** Generator-based composition for Result types. */
gen,
/** Wraps Promise<Result> to be yieldable in async Result.gen blocks. */
await: resultAwait,
/** Wraps array of Promise<Result> for parallel execution with tuple typing. */
awaitAll: resultAwaitAll,
/** Wraps sync Result to be yieldable in Result.gen blocks. */
run,
/** Splits array of Results into tuple of [okValues, errorValues]. */
partition,
/** Flattens nested Result into single Result. */
flatten,
} as const;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment