add disjoint union type
This commit is contained in:
parent
d8bd69528b
commit
272ffd6c58
1 changed files with 99 additions and 0 deletions
99
src/union.js
Normal file
99
src/union.js
Normal 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)
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue