Skip to content

Instantly share code, notes, and snippets.

@baptistemanson
Created March 1, 2023 12:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save baptistemanson/4bbbfed4aed66ae34dfa9ef990f56dc8 to your computer and use it in GitHub Desktop.
Save baptistemanson/4bbbfed4aed66ae34dfa9ef990f56dc8 to your computer and use it in GitHub Desktop.
interface Jsonable {
[x: string]: string | number | boolean | Date | Jsonable | Jsonable[];
}
// errors that can be discriminated based on a type,
// contain a message
// and can have context extra info that are serializable to json.
interface IdeaError<ErrorType> {
type: ErrorType;
message: string;
extra: Jsonable;
}
type ResultOk<T> = { ok: true; val: T };
type ResultError<ErrorType> = { ok: false; err: IdeaError<ErrorType> };
export type Result<T, ErrorType> = ResultOk<T> | ResultError<ErrorType>;
// mapped type of the content of a result
type Unwrap<Res, T, ErrorType> = Res extends ResultOk<T>
? T
: Res extends ResultError<ErrorType>
? IdeaError<ErrorType>
: never;
// Utility class.
class Res {
// Builds a ResultOk
static ok<T>(value: T): ResultOk<T> {
return { ok: true, val: value };
}
// Builds a ResultError with a stack trace
static error<ErrorType>(
type: ErrorType,
message = "",
extra: Jsonable = {}, // weonly want extra info that can be serialized
): ResultError<ErrorType> {
return {
ok: false,
err: {
type,
message,
extra: {
stack: new Error().stack ?? "couldnt get a stack trace",
...extra,
},
},
};
}
// Gets the value inside the Result.
// Will throw if the Result is an error.
// if it a way to go from Result back to Exception
static unwrap<T, ErrorType>(
r: Result<T, ErrorType>,
): Unwrap<Result<T, ErrorType>, T, ErrorType> {
if (r.ok) {
return r.val;
} else {
throw r;
}
}
// Wraps a function (that may throw) into a Result.
static async wrap<T>(b: () => T): Promise<Result<T, "WrappedError">> {
try {
return Res.ok(await b());
} catch (e) {
if (e instanceof Error)
return Res.error("WrappedError", e.message, {
stack: e.stack ?? "unknown",
});
else {
let message = "Unknown error";
if (typeof e === "string") message = e;
return Res.error("WrappedError", message);
}
}
}
// Unpacks the content of a Result and apply f on it.
static map<T1, T2, ErrorType>(
r: Result<T1, ErrorType>,
mapFn: (v: T1) => T2,
): Result<T2, ErrorType> {
return r.ok ? Res.ok(mapFn(r.val)) : r;
}
static flatMap<T1, T2, ErrorType1, ErrorType2>(
r: Result<T1, ErrorType1>,
mapFn: (v: T1) => Result<T2, ErrorType2>,
): Result<T2, ErrorType1 | ErrorType2> {
return r.ok ? mapFn(r.val) : r;
}
// curried version of map to be able to easily use on arrays.
static mapCurr<T1, T2, ErrorType>(
f: (v: T1) => T2,
): (r: Result<T1, ErrorType>) => Result<T2, ErrorType> {
return (r) => (r.ok ? Res.ok(f(r.val)) : r);
}
static flatMapCurr<T1, T2, ErrorType1, ErrorType2>(
f: (v: T1) => Result<T2, ErrorType2>,
): (r: Result<T1, ErrorType1>) => Result<T2, ErrorType1 | ErrorType2> {
return (r) => (r.ok ? f(r.val) : r);
}
}
function assertUnreachable(_: never): never {
throw new Error("Didn't expect to get here");
}
enum SystemErrors {
IO = "IO",
OutofBound = "OutOfBound",
Segfault = "Segfault",
}
enum DeveloperErrors {
RaceCondition = "RaceCondition",
DeadSW = "DeadSw",
}
function getNumbers(): Result<
number,
| SystemErrors.IO
| SystemErrors.Segfault // we can declare errors we dont yet return, but callees need to handle, which is cool
| DeveloperErrors
>[] {
return [
Res.ok(10),
Res.error(SystemErrors.IO, "io error"),
Res.error(DeveloperErrors.RaceCondition, "race condition"),
Res.error(DeveloperErrors.DeadSW, "sw not reachable"),
];
}
// gotcha
// - TS isn't good with switch case narrowing
// - error types enum defined separately can overlap
function doubleFirst(): Result<number, DeveloperErrors> {
const results = getNumbers();
if (results.length == 0) return Res.ok(0); // early return example
for (const r of results) {
if (r.ok) {
return Res.ok(r.val * r.val);
} else {
switch (
r.err.type // pattern matching
) {
case SystemErrors.IO:
case SystemErrors.Segfault:
return Res.error(DeveloperErrors.DeadSW, r.err.message); // transmute
case DeveloperErrors.DeadSW:
case DeveloperErrors.RaceCondition:
return r as ResultError<DeveloperErrors>; // narrowed forward requires explicit typing
default:
return assertUnreachable(r.err.type); // compile error on non exhaustive error handling
}
}
}
return Res.error(DeveloperErrors.RaceCondition); // ts limitation on pattern matching
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment