casework
Version:
Tag your evidence. Switch leads. Every case closed.
135 lines (120 loc) • 5.59 kB
text/typescript
/** 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;