Last active
November 23, 2025 03:02
-
-
Save znck/24ea135632523d8ef06c805f8a3259b7 to your computer and use it in GitHub Desktop.
A simple Result for TypeScript
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
| interface Success<T> extends PromiseLike<T> { | |
| readonly ok: true | |
| readonly value: T | |
| [Symbol.iterator](): Generator<never, T, unknown> | |
| } | |
| interface Failure<E extends Error> extends PromiseLike<never> { | |
| readonly ok: false | |
| readonly error: E | |
| [Symbol.iterator](): Generator<never, never, unknown> | |
| } | |
| /** | |
| * Result of an operation that can succeed or fail. | |
| */ | |
| export type Result<T, E extends Error = Error> = Success<T> | Failure<E> | |
| export const Result = { | |
| success, | |
| failure, | |
| gen, | |
| all, | |
| [Symbol.hasInstance](instance: unknown): instance is Result<unknown, Error> { | |
| return instance instanceof ResultImpl | |
| }, | |
| } | |
| // #region Core Implementation | |
| class ResultImpl<T, E extends Error> implements PromiseLike<T> { | |
| readonly ok: boolean | |
| readonly value: T | undefined | |
| readonly error: E | undefined | |
| constructor(ok: boolean, value?: T, error?: E) { | |
| this.ok = ok | |
| this.value = value | |
| this.error = error | |
| } | |
| then<TResult1 = T, TResult2 = never>( | |
| onFulfilled?: | |
| | ((value: T) => TResult1 | PromiseLike<TResult1>) | |
| | undefined | |
| | null, | |
| onRejected?: | |
| | ((reason: E) => TResult2 | PromiseLike<TResult2>) | |
| | undefined | |
| | null, | |
| ): Promise<TResult1 | TResult2> { | |
| const promise = this.ok | |
| ? Promise.resolve(this.value as T) | |
| : Promise.reject(this.error as E) | |
| if (onFulfilled || onRejected) { | |
| return promise.then(onFulfilled, onRejected) | |
| } | |
| return promise as Promise<TResult1 | TResult2> | |
| } | |
| *[Symbol.iterator](): Generator<never, T, unknown> { | |
| if (this.ok) { | |
| return this.value as T | |
| } else { | |
| throw this.error | |
| } | |
| } | |
| } | |
| function success(): Success<void> | |
| function success<T>(value: T): Success<T> | |
| function success(value?: unknown): Success<unknown> { | |
| return new ResultImpl<unknown, never>(true, value) as Success<unknown> | |
| } | |
| function failure<E extends Error>(error: E): Failure<E> { | |
| return new ResultImpl<never, E>(false, undefined, error) as Failure<E> | |
| } | |
| // #endregion | |
| // #region Result.gen | |
| /** | |
| * Runs a generator function and captures its result. | |
| * Use yield* to unwrap Result values or throw on errors. | |
| */ | |
| function gen<T, E extends Error>( | |
| generator: (this: null) => Generator<never, Result<T, E>, unknown>, | |
| ): Result<T, E> | |
| function gen<T, E extends Error, ThisArg>( | |
| generator: (this: ThisArg) => Generator<never, Result<T, E>, unknown>, | |
| thisArg: ThisArg, | |
| ): Result<T, E> | |
| function gen( | |
| generator: (this: unknown) => Generator<never, Result<unknown>, unknown>, | |
| thisArg: unknown = null, | |
| ): Result<unknown> { | |
| try { | |
| const iter = generator.call(thisArg) | |
| while (true) { | |
| const next = iter.next() | |
| if (next.done) { | |
| return next.value | |
| } | |
| } | |
| } catch (error) { | |
| return failure(error instanceof Error ? error : new Error(String(error))) | |
| } | |
| } | |
| // #endregion | |
| // #region Result.all | |
| class AggregateError extends Error { | |
| public readonly errors: Error[] | |
| constructor(errors: Error[], message?: string) { | |
| super(message ?? `AggregateError: ${errors.length} error(s) occurred`) | |
| this.name = 'AggregateError' | |
| this.errors = errors | |
| } | |
| } | |
| type SuccessTuple<T extends readonly Result<any, any>[]> = { | |
| [K in keyof T]: T[K] extends Result<infer S, any> ? S : never | |
| } | |
| /** | |
| * Combines multiple results. Returns success if all succeed, or failure with all errors. | |
| */ | |
| function all<const T extends readonly Result<any>[]>( | |
| results: T, | |
| ): Result<SuccessTuple<T>, AggregateError> | |
| function all<const T extends Record<string, Result<any>>>( | |
| results: T, | |
| ): Result< | |
| { | |
| [K in keyof T]: T[K] extends Result<infer S, any> ? S : never | |
| }, | |
| AggregateError | |
| > | |
| function all<T, E extends Error>( | |
| results: Iterable<Result<T, E>>, | |
| ): Result<T[], AggregateError> | |
| function all( | |
| results: Iterable<Result<any, Error>> | Record<string, Result<any, Error>>, | |
| ): Result<any, AggregateError> { | |
| if (Array.isArray(results)) { | |
| const successValues: any[] = [] | |
| const failureErrors: Error[] = [] | |
| for (const result of results) { | |
| if (result.ok) { | |
| successValues.push(result.value) | |
| } else { | |
| failureErrors.push(result.error) | |
| } | |
| } | |
| if (failureErrors.length > 0) { | |
| return failure(new AggregateError(failureErrors)) | |
| } | |
| return success(successValues) | |
| } else { | |
| const successValues: Record<string, any> = {} | |
| const failureErrors: Error[] = [] | |
| for (const [key, result] of Object.entries(results)) { | |
| if (result.ok) { | |
| successValues[key] = result.value | |
| } else { | |
| failureErrors.push(result.error) | |
| } | |
| } | |
| if (failureErrors.length > 0) { | |
| return failure(new AggregateError(failureErrors)) | |
| } | |
| return success(successValues) | |
| } | |
| } | |
| // #endregion |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment