Last active
November 14, 2024 17:12
-
-
Save bryanmylee/564a872a16db330f716bf6e21a791f7e to your computer and use it in GitHub Desktop.
Handle errors more declaratively without try catch
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
/** | |
* Handle errors more declaratively without try catch. | |
* | |
* Non-async functions returning `T` will return `Result<T, unknown>`, and | |
* async functions returning `Promise<T>` will return | |
* `Promise<Result<T, unknown>>`. | |
* | |
* Supports overloaded function definitions. Refer to | |
* {@link OverloadedResultFn}. | |
* | |
* ``` | |
* try { | |
* const data = doSomething(30); | |
* } catch (err) { | |
* console.log(err); | |
* } | |
* // becomes | |
* const result = tryResult(doSomething)(30); | |
* if (result.ok) { | |
* const data = result.value; | |
* } else { | |
* console.log(result.err); | |
* } | |
* ``` | |
* | |
* @param fn A throwable function to run. | |
* @returns A function that returns a Result instead of throwing an error. | |
*/ | |
export function tryResult<TFn extends CallableFunction>( | |
fn: TFn | |
): OverloadedResultFn<TFn> { | |
return function (...args: unknown[]) { | |
try { | |
const returnValue = fn(...args); | |
if (isPromise(returnValue)) { | |
return returnValue | |
.then((awaitedValue) => flattenTryResult(awaitedValue, fn)) | |
.catch((err) => Err(err)); | |
} | |
return flattenTryResult(returnValue, fn); | |
} catch (err) { | |
return Err(err); | |
} | |
} as OverloadedResultFn<TFn>; | |
} | |
export function Ok<T>(value: T): Result<T, never> { | |
return { ok: true, value, " $isResult": result_symbol }; | |
} | |
export function Err<E>(err: E): Result<never, E> { | |
return { ok: false, err, " $isResult": result_symbol }; | |
} | |
export function isResult(value: unknown): value is Result<unknown, unknown> { | |
return ( | |
typeof value === "object" && | |
value != null && | |
" $isResult" in value && | |
value[" $isResult"] === result_symbol | |
); | |
} | |
export function isOk<T>(value: Result<T, unknown>): value is ResultOk<T> { | |
return value.ok; | |
} | |
export function isErr<E>(value: Result<unknown, E>): value is ResultErr<E> { | |
return !value.ok; | |
} | |
export type Result<T, E> = ResultOk<T> | ResultErr<E>; | |
export type ResultOk<T> = { | |
ok: true; | |
value: T; | |
} & ResultBrand; | |
export type ResultErr<E> = { | |
ok: false; | |
err: E; | |
} & ResultBrand; | |
const result_symbol = Symbol("Result"); | |
type ResultBrand = { | |
" $isResult": typeof result_symbol; | |
}; | |
/* | |
* +===================+ | |
* | UTILITY FUNCTIONS | | |
* +===================+ | |
*/ | |
function isPromise(value: unknown): value is Promise<unknown> { | |
return value instanceof Promise; | |
} | |
/** | |
* Similar to `Ok(T)` except it recursively unwraps `T` if `T` is `Result`. | |
* | |
* Internal use only. | |
*/ | |
function flattenTryResult( | |
value: unknown, | |
fn: CallableFunction | |
): Result<unknown, unknown> { | |
if (isResult(value)) { | |
const fnName = fn.name || fn.toString(); | |
console.warn( | |
`Unnecessary \`tryResult(${fnName})(...)\`` | |
); | |
return value; | |
} | |
return Ok(value); | |
} | |
/* | |
* +===============+ | |
* | UTILITY TYPES | | |
* +===============+ | |
*/ | |
/** | |
* Due to a lack of higher-kinded types, we cannot capture an arbitrary number | |
* of overloads. | |
* | |
* Add as many overloaded types as needed. | |
* | |
* Internal use only. | |
*/ | |
type OverloadedResultFn<TFn> = TFn extends { | |
(...args: infer TArgs1): infer TReturn1; | |
(...args: infer TArgs2): infer TReturn2; | |
} | |
? { | |
(...args: TArgs1): ResultAnyOrPromise<TReturn1>; | |
(...args: TArgs2): ResultAnyOrPromise<TReturn2>; | |
} | |
: never; | |
/** | |
* Internal use only. | |
*/ | |
type ResultAnyOrPromise<TReturn> = | |
IsStrictlyAny<TReturn> extends true | |
? ResultFlatten<TReturn> | |
: TReturn extends Promise<infer TAwaited> | |
? Promise<ResultFlatten<TAwaited>> | |
: ResultFlatten<TReturn>; | |
/** | |
* Recursively reduces `Result<Result<T, E>, unknown>` into `Result<T, unknown>`. | |
* | |
* Internal use only. | |
*/ | |
type ResultFlatten<T> = | |
T extends ResultOk<infer TNested> | |
? ResultFlatten<TNested> | |
: // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
T extends ResultErr<any> | |
? never | |
: Result<T, unknown>; | |
// From: https://stackoverflow.com/a/50375286/62076 | |
type UnionToIntersection<U> = | |
// eslint-disable-next-line @typescript-eslint/no-explicit-any | |
(U extends any ? (k: U) => void : never) extends (k: infer I) => void | |
? I | |
: never; | |
// If T is `any` a union of both side of the condition is returned. | |
type UnionForAny<T> = T extends never ? "A" : "B"; | |
// Returns true if type is any, or false for any other type. | |
export type IsStrictlyAny<T> = | |
UnionToIntersection<UnionForAny<T>> extends never ? true : false; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment