Skip to content

Instantly share code, notes, and snippets.

@baetheus
Last active September 18, 2022 23:47
Show Gist options
  • Save baetheus/ea2ce7727a132ab05606b5dd810d0877 to your computer and use it in GitHub Desktop.
Save baetheus/ea2ce7727a132ab05606b5dd810d0877 to your computer and use it in GitHub Desktop.
Playing around with a generic tagged error
// 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