diff --git a/src/union.js b/src/union.js new file mode 100644 index 0000000..ef14b22 --- /dev/null +++ b/src/union.js @@ -0,0 +1,99 @@ +/** + * @template R + * @typedef {(...args: any[]) => R} Fn + */ + +/** + * @template R + * @typedef {() => R} EmptyFn + */ + +/** + * @template R + * @callback match + * @param {Record> & Partial>>} pattern + * @returns {R} + * + * @throws MatchError + */ + +/** + * @typedef Variant + * @property {match} match + */ + +/** + * @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') + +class MatchError extends Error { + /** @param {PropertyKey} name */ + constructor(name) { + super(`unmatched arm in match: ${name.toString()}`) + } +} + +/** + * @param {PropertyKey} variant + * @param {PropertyKey} variant + * @returns {(...values: any[]) => Variant} + */ +const Variant = (type, variant) => (...values) => ({ + [Tag]: type, + match(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 MatchError(variant) + } + } +}) + +/** + * @template {object} T + * @typedef {T[keyof T]} Tuple + */ + +/** + * @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) +} +