UNPKG

casework

Version:

Tag your evidence. Switch leads. Every case closed.

135 lines (120 loc) 5.59 kB
/** Docs WIP, but this core is highly functional as-is */ export interface Tag<T extends string, Data = undefined> { tag: T; data: Data; } /** Docs WIP, but this core is highly functional as-is */ export function Tag<T extends string>(tag: T): Tag<T, undefined>; export function Tag<T extends string, D>(tag: T, data: D): Tag<T, D>; export function Tag<T extends string, D>(tag: T, data?: D): Tag<T, any> { return { tag, data } as Tag<T, D>; } export type TagFrom<T> = [T] extends [Tag<infer TagStr, any>] ? TagStr : never; export type DataFrom<T> = [T] extends [Tag<any, infer Data>] ? Data : never; type MatchHandlers<Tags extends Tag<string, any>, Returned> = { [K in TagFrom<Tags>]: (data: Extract<Tags, Tag<K, any>>["data"]) => Returned; }; /** Docs WIP, but this core is highly functional as-is */ export function match<Returned, Tags extends Tag<string, any>, Handlers extends MatchHandlers<Tags, Returned>>( tagged: Tags, handlers: Handlers ): ReturnType<Handlers[keyof Handlers]> { //all the type gymnastics have been done in the rest of the signature, and the inference and usage of this is perfect as is. we can ignore this key-lookup error here. //@ts-ignore return handlers[tagged.tag](tagged.data); } /** Docs WIP, but this core is highly functional as-is */ export function match_into<Returned>() { //identical to match except doesn't have the 3rd type argument. repeats everything else return { from: function <Tags extends Tag<string, any>, Handlers extends MatchHandlers<Tags, Returned>>( tagged: Tags, handlers: Handlers ): Returned { //@ts-ignore - see `match` return handlers[tagged.tag](tagged.data); }, }; } /** You may have a need to create a specific kind of tag, constrained by a type up-front. * * This is common when returning from a function that has an annotated return type: * - You've defined your return type which is a union of various Tags * - If you attempt to return the wrong shape of Tag, you'll be type checked. * - However, the pain point is right at the `return` site when you're creating the Tag to be returned * - You may get limited help from your tooling. * - Often, the tag is constrained to the correct string literal values, but * - You get no help with the associated data, * - or the tooling can't narrow and suggests any of the data from any tag in the union * * * This `from` function allows you to set that up, and get a `MakeTag` function out. `MakeTag` is exactly like `Tag`, * other than being type constrained * * ## EXAMPLE: * ```ts * type LatestUser = * | Tag<"NoUser"> * | Tag<"User", {name: string, id: number}> * | Tag<"QueryFailed", {message: string, timestamp: number}> * * function queryLatestUser(): LatestUser { * // In this function body, at various spots you may want to return with the proper Tag. * // Again, the return type placed on this function will still typecheck that it's valid, * // but as you're creating the tag at the `return` site, you get less direct help. * // * // So if you started to do * return Tag("QueryFailed", { //here you don't assistance or constraints, * // OR you do but it can't narrow, so it suggests {name: string} and {id: number} * // But ideally you'd be guided to only {message: string, timestamp: number}; * // That's exactly where this function steps in to help * }) * * return from<LatestUser>().MakeTag("QueryFailed", { //here you DO get assistance * * } * * ``` * * You also may want to re-use this repeatedly throughout a function that returns many variants. * * You can prepare the `MakeTag` function beforehand and then use it repeatedly. * * ```ts * const { MakeTag } = from<LatestUser>() * ``` * * * NOTE: It's possible that a more complex conditional type on the core Tag function may be able to do this properly in the future (eliminating the need for this helper). That avenue will be explored. */ export function from<Tags extends Tag<string, any>>() { function MakeTag<K extends TagFrom<Tags>, FullTag extends Extract<Tags, Tag<K, any>>>( tag: K, ...args: FullTag["data"] extends undefined ? [] : [FullTag["data"]] ): FullTag { return { tag: tag, data: args[0] } as FullTag; } return { MakeTag, }; } /* ~~ ~~ ~~ */ // ~ With really strong primitives like Tag, powerful stuff like Result can emerge, and it's just a convention + convenience /** Docs WIP, but this core is highly functional as-is */ export interface Ok<Output> extends Tag<"Ok", Output> {} /** Docs WIP, but this core is highly functional as-is */ export interface Fail<Failure> extends Tag<"Failed", Failure> {} /** Docs WIP, but this core is highly functional as-is */ export type Result<Output, Failure> = Ok<Output> | Fail<Failure>; /** Docs WIP, but this core is highly functional as-is */ export const Ok = <Output>(output: Output): Result<Output, never> => Tag("Ok", output); /** Docs WIP, but this core is highly functional as-is */ export const Fail = <Failure>(failure: Failure): Result<never, Failure> => Tag("Failed", failure); /** Docs WIP, but this core is highly functional as-is */ export type OutputFrom<R> = [R] extends [Result<infer Output, any>] ? Output : never; /** Docs WIP, but this core is highly functional as-is */ export type FailureFrom<R> = [R] extends [Result<any, infer Failure>] ? Failure : never;