UNPKG

@proem/variant

Version:

proem package for handling algebraic data types

110 lines (97 loc) 2.69 kB
import * as array from '@proem/array' export interface Variant<Tag extends string> { type: Tag } export type VariantTags<V extends Variant<string>> = V extends Variant< infer Tags > ? Tags : never /** * Returns the provided tags in an tuple. */ export function tags<Tags extends string[]>(...tags: Tags): Tags { return tags } /** * Type guard that refine the union type to a more specific one. * * Returns true if the variant has one of the provided tags. */ export function oneOf<V extends Variant<string>, Tags extends V['type']>( variant: V, tags: Tags[], ): variant is OneOf<V, Tags> { return array.includes(tags, variant.type) } /** * Refine the Variant type to include only the specified tags. */ export type OneOf< V extends Variant<string>, Tag extends string > = V extends Variant<Tag> ? V : never /** * Create a matcher that maps a Variant to a type. * * Requires that all cases are handled, or that a default case * is provided as a second argument. * * @param cases * @param or default case */ export function map<V extends Variant<string>, A>( cases: Cases<V, A>, ): (variant: V) => A export function map<V extends Variant<string>, A>( cases: PartialCases<V, A>, or: (variant: V) => A, ): (variant: V) => A export function map<V extends Variant<string>, A>( cases: PartialCases<V, A>, or?: (variant: V) => A, ): (variant: V) => A { return variant => { const caseFn = cases[variant.type as keyof Cases<V, A>] if (!caseFn) { if (!or) { throw Error(`No match case found for "${variant.type}"`) } return or(variant) } return caseFn(variant as any) } } export type CaseBody<V extends Variant<string>, A> = (adt: V) => A export type Cases<V extends Variant<string>, A> = { [P in V['type']]: CaseBody<OneOf<V, P>, A> } export type PartialCases<V extends Variant<string>, A> = { [P in V['type']]?: CaseBody<OneOf<V, P>, A> } /** * Create a reducer function that takes a state value as first * argument, and a Variant as a second argument. * * Returns the provided state if no case is found. * * Works well with React's useReducer hook. */ export function reducer<State, V extends Variant<string>>( caseReducers: CaseReducers<State, V>, ): Reducer<State, V> { return (state, variant) => { const arm = caseReducers[variant.type as keyof typeof caseReducers] if (!arm) { return state } return arm(state, variant as any) } } export type Reducer<State, V extends Variant<string>> = ( state: State, variant: V, ) => State export type CaseReducers<State, V extends Variant<string>> = { [P in VariantTags<V>]: Reducer<State, OneOf<V, P>> }