Last active
October 14, 2025 09:01
-
-
Save nandordudas/7ba83657a05ed0106366d122d42fd044 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function _createError<E extends Error>(reason: unknown): E { | |
| if (reason instanceof Error) | |
| return reason as E | |
| return new Error(String(reason)) as E | |
| } | |
| /** | |
| * Result type for Railway-Oriented Programming pattern. | |
| * Represents either a successful value or an error. | |
| * | |
| * @example | |
| * ```ts | |
| * const result: Result<number> = ok(42) | |
| * const error: Result<number> = err(new Error('Failed')) | |
| * ``` | |
| */ | |
| export type Result<T, E extends Error = Error> | |
| = | { data: T, error: null } | |
| | { data: null, error: E } | |
| /** | |
| * Creates a successful Result with data. | |
| * | |
| * @example | |
| * ```ts | |
| * const result = ok(42) | |
| * console.log(result.data) // 42 | |
| * ``` | |
| */ | |
| export function ok<T>(data: T): Result<T, never> { | |
| return { data, error: null } | |
| } | |
| /** | |
| * Creates an error Result. | |
| * | |
| * @example | |
| * ```ts | |
| * const result = err(new Error('Something went wrong')) | |
| * console.log(result.error.message) // 'Something went wrong' | |
| * ``` | |
| */ | |
| export function err<E extends Error>(error: E): Result<never, E> { | |
| return { data: null, error } | |
| } | |
| /** | |
| * Wraps a function call in try-catch and returns a Result. | |
| * Handles both synchronous and asynchronous functions. | |
| * | |
| * @param fn - The function to execute | |
| * @param mapError - Optional function to transform the caught error | |
| * | |
| * @example | |
| * ```ts | |
| * // Sync example | |
| * const result = tryCatch(() => JSON.parse('{"valid": "json"}')) | |
| * // result = { data: { valid: 'json' }, error: null } | |
| * | |
| * const error = tryCatch(() => JSON.parse('invalid')) | |
| * // error = { data: null, error: SyntaxError } | |
| * | |
| * // Async example | |
| * const asyncResult = await tryCatch(async () => { | |
| * const response = await fetch('/api/data') | |
| * return response.json() | |
| * }) | |
| * | |
| * // Custom error mapping | |
| * const customError = tryCatch( | |
| * () => riskyOperation(), | |
| * (err) => new CustomError(String(err)) | |
| * ) | |
| * ``` | |
| */ | |
| export function tryCatch<T, E extends Error = Error>( | |
| fn: () => Promise<T>, | |
| mapError?: (reason: unknown) => E | |
| ): Promise<Result<T, E>> | |
| export function tryCatch<T, E extends Error = Error>( | |
| fn: () => T, | |
| mapError?: (reason: unknown) => E | |
| ): Result<T, E> | |
| export function tryCatch<T, E extends Error = Error>( | |
| fn: () => T | Promise<T>, | |
| mapError = _createError<E>, | |
| ): Result<T, E> | Promise<Result<T, E>> { | |
| try { | |
| const result = fn() | |
| if (result instanceof Promise) { | |
| return result | |
| .then(data => ({ data, error: null } as const)) | |
| .catch((reason: unknown) => ({ data: null, error: mapError(reason) } as const)) | |
| } | |
| return { data: result, error: null } | |
| } | |
| catch (reason: unknown) { | |
| return { data: null, error: mapError(reason) } | |
| } | |
| } | |
| /** | |
| * Unwraps a Result in a generator context, yielding errors and returning data. | |
| * | |
| * Note: The generator yields control when encountering an error. The return | |
| * value after yield is never actually used - run() catches the error before | |
| * the generator continues. | |
| * | |
| * @example | |
| * ```ts | |
| * const result = run(function* () { | |
| * const a = yield* unwrap(ok(5)) | |
| * const b = yield* unwrap(err(new Error('Stop here'))) | |
| * const c = yield* unwrap(ok(3)) // Never executes | |
| * return a + b + c | |
| * }) | |
| * // result = { data: null, error: Error('Stop here') } | |
| * ``` | |
| */ | |
| export function unwrap<T, E extends Error = Error>( | |
| result: Result<T, E>, | |
| ): Generator<Result<never, E>, T, any> { | |
| return (function* () { | |
| if (result.error !== null) { | |
| yield err(result.error) | |
| throw new Error('Unreachable: error should be caught by run()') | |
| } | |
| return result.data! | |
| })() | |
| } | |
| /** | |
| * Async version of unwrap for use with async generators. | |
| * Unwraps a Result promise in an async generator context. | |
| * | |
| * @example | |
| * ```ts | |
| * const asyncResult1 = Promise.resolve(ok(5)) | |
| * const asyncResult2 = Promise.resolve(ok(10)) | |
| * | |
| * const final = await run(async function* () { | |
| * const a = yield* unwrapAsync(asyncResult1) | |
| * const b = yield* unwrapAsync(asyncResult2) | |
| * return a + b | |
| * }) | |
| * // final = { data: 15, error: null } | |
| * ``` | |
| */ | |
| export async function* unwrapAsync<T, E extends Error = Error>( | |
| resultPromise: Promise<Result<T, E>>, | |
| ): AsyncGenerator<Result<never, E>, T, any> { | |
| const result = await resultPromise | |
| if (result.error !== null) { | |
| yield err(result.error) | |
| throw new Error('Unreachable: error should be caught by run()') | |
| } | |
| return result.data! | |
| } | |
| /** | |
| * Runs a generator function that uses unwrap/unwrapAsync for error handling. | |
| * Implements Railway-Oriented Programming pattern - stops at first error. | |
| * | |
| * @example | |
| * ```ts | |
| * const divide = (a: number, b: number): Result<number> => | |
| * b === 0 ? err(new Error('Division by zero')) : ok(a / b) | |
| * | |
| * const result = run(function* () { | |
| * const a = yield* unwrap(ok(10)) | |
| * const b = yield* unwrap(ok(2)) | |
| * const c = yield* unwrap(divide(a, b)) | |
| * return c * 3 | |
| * }) | |
| * // result = { data: 15, error: null } | |
| * | |
| * const errorResult = run(function* () { | |
| * const a = yield* unwrap(ok(10)) | |
| * const b = yield* unwrap(divide(a, 0)) // Error here | |
| * return b * 3 // Never executes | |
| * }) | |
| * // errorResult = { data: null, error: Error('Division by zero') } | |
| * | |
| * // Async example | |
| * const asyncResult = await run(async function* () { | |
| * const data = yield* unwrapAsync(fetchData()) | |
| * const processed = yield* unwrap(processData(data)) | |
| * return processed | |
| * }) | |
| * | |
| * // With context binding (this) | |
| * class MyClass { | |
| * value = 42 | |
| * async process() { | |
| * return await run(async function* (this: MyClass) { | |
| * const result = yield* unwrapAsync(someAsyncOp()) | |
| * return this.value + result | |
| * }, this) | |
| * } | |
| * } | |
| * ``` | |
| */ | |
| // Overload: sync generator without context | |
| export function run<T, E extends Error = Error>( | |
| generatorFn: (this: void) => Generator<Result<any, E>, T, any>, | |
| ): Result<T, E> | |
| // Overload: sync generator with context | |
| export function run<T, C, E extends Error = Error>( | |
| generatorFn: (this: C) => Generator<Result<any, E>, T, any>, | |
| context: C, | |
| ): Result<T, E> | |
| // Overload: async generator without context | |
| export function run<T, E extends Error = Error>( | |
| generatorFn: (this: void) => AsyncGenerator<Result<any, E>, T, any>, | |
| ): Promise<Result<T, E>> | |
| // Overload: async generator with context | |
| export function run<T, C, E extends Error = Error>( | |
| generatorFn: (this: C) => AsyncGenerator<Result<any, E>, T, any>, | |
| context: C, | |
| ): Promise<Result<T, E>> | |
| // Implementation | |
| // eslint-disable-next-line complexity | |
| export function run<T, E extends Error = Error>( | |
| generatorFn: (this: any) => Generator<Result<any, E>, T, any> | AsyncGenerator<Result<any, E>, T, any>, | |
| context?: any, | |
| ): Result<T, E> | Promise<Result<T, E>> { | |
| try { | |
| const iterator = context ? generatorFn.call(context) : generatorFn() | |
| if (Symbol.asyncIterator in iterator) { | |
| return (async () => { | |
| try { | |
| let state = await iterator.next() | |
| while (!state.done) { | |
| const result = state.value | |
| if (result.error !== null) | |
| return { data: null, error: result.error } | |
| state = await iterator.next(result.data as any) | |
| } | |
| return { data: state.value, error: null } | |
| } | |
| catch (reason: unknown) { | |
| return { data: null, error: _createError<E>(reason) } | |
| } | |
| })() | |
| } | |
| let state = iterator.next() | |
| while (!state.done) { | |
| const result = state.value | |
| if (result.error !== null) | |
| return { data: null, error: result.error } | |
| state = iterator.next(result.data as any) | |
| } | |
| return { data: state.value, error: null } | |
| } | |
| catch (reason: unknown) { | |
| return { data: null, error: _createError<E>(reason) } | |
| } | |
| } | |
| /** | |
| * Type guard to check if a Result is successful. | |
| * Narrows the type to allow safe access to data property. | |
| * | |
| * @example | |
| * ```ts | |
| * const result: Result<number> = ok(42) | |
| * | |
| * if (isOk(result)) { | |
| * console.log(result.data) // TypeScript knows data is number | |
| * } | |
| * ``` | |
| */ | |
| export function isOk<T, E extends Error = Error>( | |
| result: Result<T, E>, | |
| ): result is { data: T, error: null } { | |
| return result.error === null | |
| } | |
| /** | |
| * Type guard to check if a Result is an error. | |
| * Narrows the type to allow safe access to error property. | |
| * | |
| * @example | |
| * ```ts | |
| * const result: Result<number> = err(new Error('Failed')) | |
| * | |
| * if (isErr(result)) { | |
| * console.log(result.error.message) // TypeScript knows error is E | |
| * } | |
| * ``` | |
| */ | |
| export function isErr<T, E extends Error = Error>( | |
| result: Result<T, E>, | |
| ): result is { data: null, error: E } { | |
| return result.error !== null | |
| } | |
| /** | |
| * Transforms the success value of a Result using a mapping function. | |
| * If the Result is an error, returns the error unchanged. | |
| * | |
| * @example | |
| * ```ts | |
| * const result = ok(5) | |
| * const mapped = map(result, x => x * 2) | |
| * // mapped = { data: 10, error: null } | |
| * | |
| * const error = err(new Error('Failed')) | |
| * const mappedError = map(error, x => x * 2) | |
| * // mappedError = { data: null, error: Error('Failed') } | |
| * ``` | |
| */ | |
| export function map<T, U, E extends Error = Error>( | |
| result: Result<T, E>, | |
| fn: (data: T) => U, | |
| ): Result<U, E> { | |
| if (result.error !== null) | |
| return err(result.error) | |
| return ok(fn(result.data as T)) | |
| } | |
| /** | |
| * Chains Result-returning operations together (also known as bind/flatMap). | |
| * If the first Result is an error, returns that error. Otherwise, applies | |
| * the function to the success value. | |
| * | |
| * @example | |
| * ```ts | |
| * const divide = (a: number, b: number): Result<number> => | |
| * b === 0 ? err(new Error('Division by zero')) : ok(a / b) | |
| * | |
| * const result1 = ok(10) | |
| * const result2 = flatMap(result1, x => divide(x, 2)) | |
| * // result2 = { data: 5, error: null } | |
| * | |
| * const result3 = flatMap(result1, x => divide(x, 0)) | |
| * // result3 = { data: null, error: Error('Division by zero') } | |
| * ``` | |
| */ | |
| export function flatMap<T, U, E extends Error = Error>( | |
| result: Result<T, E>, | |
| fn: (data: T) => Result<U, E>, | |
| ): Result<U, E> { | |
| if (result.error !== null) | |
| return err(result.error) | |
| return fn(result.data as T) | |
| } | |
| /** | |
| * Unwraps a Result, returning the success value or a default value if error. | |
| * | |
| * @example | |
| * ```ts | |
| * const success = ok(42) | |
| * unwrapOr(success, 0) // 42 | |
| * | |
| * const failure = err(new Error('Failed')) | |
| * unwrapOr(failure, 0) // 0 | |
| * ``` | |
| */ | |
| export function unwrapOr<T, E extends Error = Error>( | |
| result: Result<T, E>, | |
| defaultValue: T, | |
| ): T { | |
| return result.error !== null ? defaultValue : result.data as T | |
| } | |
| /** | |
| * Unwraps a Result, returning the success value or throwing the error. | |
| * Use this when you want to convert Result-based error handling to | |
| * exception-based error handling. | |
| * | |
| * @throws The error from the Result if it's an error | |
| * | |
| * @example | |
| * ```ts | |
| * const success = ok(42) | |
| * unwrapOrThrow(success) // 42 | |
| * | |
| * const failure = err(new Error('Failed')) | |
| * unwrapOrThrow(failure) // throws Error('Failed') | |
| * ``` | |
| */ | |
| export function unwrapOrThrow<T, E extends Error = Error>( | |
| result: Result<T, E>, | |
| ): T { | |
| if (result.error !== null) | |
| throw result.error | |
| return result.data as T | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment