/** * @template R * @typedef {(...args: any[]) => R} Fn */ /** * @template R * @typedef {() => R} EmptyFn */ /** * @template R * @callback cata * @param {Record> & Partial>>} pattern * @returns {R} * * @throws MatchError */ /** * @typedef Variant * @property {cata} cata */ /** * @typedef {(...values: any[]) => Variant} VariantConstructor */ /** * @template {PropertyKey[]} Variant * @typedef {{ [key in Variant[number]]: (...values: any) => Variant }} Variants */ /** * @template {PropertyKey} T * @template {PropertyKey[]} U * @typedef {{ is: typeof is } & Variants} Union */ const Tag = Symbol('Tag') export class CatamorphismError extends Error { /** @param {PropertyKey} name */ constructor(name) { super(`unmatched arm in catamorphism: ${name.toString()}`) } } /** * @param {PropertyKey} type * @param {PropertyKey} variant * @returns {(...values: any[]) => Variant} */ const Variant = (type, variant) => (...values) => ({ [Tag]: type, cata(pattern) { if (variant in pattern) { // NOTE: this is a workaround for typescript not recognizing // that objects can be indexed with symbols return pattern[ /** @type {string} */ (variant)](...values) } else if ('_' in pattern) { return pattern._() } else { throw new CatamorphismError(variant) } } }) /** * @template T, U * @this {Union} * @param {any} other * @returns {other is Variant} */ function is(other) { return Object.hasOwn(other, Tag) && other[Tag] === this[Tag] } /** * @template {PropertyKey} const T * @template {Array} const U * @param {T} typeName * @param {...U} variantNames * @returns {Union} */ export const Union = (typeName, variantNames) => { const tag = { [Tag]: typeName, is } const variants = Object.fromEntries(variantNames.map(v => [v, Variant(typeName, v)])) const result = Object.assign(tag, variants) return /** @type {Union} */ (result) }