add disjoint union type

This commit is contained in:
Rowan 2025-04-03 16:53:17 -05:00
parent d8bd69528b
commit 272ffd6c58

99
src/union.js Normal file
View file

@ -0,0 +1,99 @@
/**
* @template R
* @typedef {(...args: any[]) => R} Fn
*/
/**
* @template R
* @typedef {() => R} EmptyFn
*/
/**
* @template R
* @callback match
* @param {Record<string, Fn<R>> & Partial<Record<'_', EmptyFn<R>>>} pattern
* @returns {R}
*
* @throws MatchError
*/
/**
* @typedef Variant
* @property {match<any>} 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<U>} 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<T, U>}
* @param {any} other
* @returns {other is Variant<U>}
*/
function is(other) {
return Object.hasOwn(other, Tag) && other[Tag] === this[Tag]
}
/**
* @template {PropertyKey} const T
* @template {Array<PropertyKey>} const U
* @param {T} typeName
* @param {...U} variantNames
* @returns {Union<T, U>}
*/
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<T, U>} */ (result)
}