Last active
September 18, 2022 23:47
-
-
Save baetheus/ea2ce7727a132ab05606b5dd810d0877 to your computer and use it in GitHub Desktop.
Playing around with a generic tagged error
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
// deno-lint-ignore-file no-explicit-any | |
import * as RSS from "https://deno.land/x/rss@0.5.6/mod.ts"; | |
import * as TE from "https://raw.githubusercontent.com/baetheus/fun/main/task_either.ts"; | |
import { pipe } from "https://raw.githubusercontent.com/baetheus/fun/main/fns.ts"; | |
// --- | |
// Try implementing a reusable error type | |
// --- | |
export type TaggedError<T extends string, A> = { | |
readonly tag: "Error"; | |
readonly type: T; | |
readonly context: A; | |
readonly error: unknown; | |
}; | |
export type AnyTaggedError = TaggedError<string, any>; | |
export const taggedError = <T extends string, A>(type: T) => | |
(error: unknown, context: A): TaggedError<T, A> => ({ | |
tag: "Error", | |
type, | |
context, | |
error, | |
}); | |
// --- | |
// Use a little creative typing to extract tag and context pairs and | |
// build a dynamically typed fold function | |
// --- | |
type ExtractTags<T> = T extends TaggedError<infer Tag, any> ? Tag : never; | |
type MatchTag<Tag, Errors> = Tag extends string | |
? Errors extends TaggedError<Tag, infer V> ? TaggedError<Tag, V> : never | |
: never; | |
type MapFunc<T, B> = T extends TaggedError<string, infer V> | |
? (error: unknown, context: V) => B | |
: never; | |
type ToRecord<T, B> = { [K in ExtractTags<T>]: MapFunc<MatchTag<K, T>, B> }; | |
export const fold = <T extends AnyTaggedError, B>(fns: ToRecord<T, B>) => | |
(ta: T): B => (fns[ta.type as keyof ToRecord<T, B>])(ta.error, ta.context); | |
/** | |
* Wrapping the errors for tryCatch tend to all look like | |
* TaggedError, so why not just tag the errors and reuse | |
* the taggedError constructor. | |
*/ | |
export const tagTryCatch = <T extends string, A extends unknown[], O>( | |
tag: T, | |
f: (...as: A) => O | PromiseLike<O>, | |
) => TE.tryCatch(f, taggedError(tag)); | |
// --- | |
// Wrap external functions with tagged errors | |
// --- | |
export const parseFeed = tagTryCatch("RssError", RSS.parseFeed); | |
export const safeFetch = tagTryCatch("FetchError", fetch); | |
export const getText = tagTryCatch("TextError", (res: Response) => res.text()); | |
export const stringify = tagTryCatch( | |
"StringifyError", | |
<A>(a: A) => JSON.stringify(a, null, 2), | |
); | |
// --- | |
// Get some xml, parse it as rss, and log it | |
// --- | |
export const run = pipe( | |
// Start with a fetch | |
safeFetch("https://hnrss.org/frontpage-blarg"), | |
// The default chain widens the left type, picking up the | |
// additional TaggedError types | |
TE.chain(getText), | |
// Parse the body text | |
TE.chain(parseFeed), | |
// Stringify feed values | |
TE.chain(stringify), | |
// Output the date | |
TE.fold( | |
// Use the taggedError fold to extract all unioned tags | |
fold({ | |
"FetchError": console.error, | |
"RssError": console.error, | |
"TextError": console.error, | |
"StringifyError": console.error, | |
}), | |
console.log, | |
), | |
); | |
// Run the program | |
await run(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment