Skip to content

Instantly share code, notes, and snippets.

@bryanmylee
Last active November 14, 2024 17:12
Show Gist options
  • Save bryanmylee/564a872a16db330f716bf6e21a791f7e to your computer and use it in GitHub Desktop.
Save bryanmylee/564a872a16db330f716bf6e21a791f7e to your computer and use it in GitHub Desktop.
Handle errors more declaratively without try catch
/**
* 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