Created
March 1, 2023 12:34
-
-
Save baptistemanson/4bbbfed4aed66ae34dfa9ef990f56dc8 to your computer and use it in GitHub Desktop.
This file contains 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 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