/ src / type.js
type.js
  1  import * as API from './api.js'
  2  import { is as isLink } from './data/link.js'
  3  
  4  /**
  5   * Checks given `value` against the given `type` and returns `true` if type
  6   * matches. Function can be used as [type predicate].
  7   *
  8   * [type predicate]: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
  9   *
 10   * @example
 11   *
 12   * ```ts
 13   * export const demo = (value: Constant) => {
 14   *   if (Type.isTypeOf(Type.String, value)) {
 15   *     // type was narrowed down to string
 16   *     console.log(value.toUpperCase())
 17   *   }
 18   * }
 19   * ```
 20   *
 21   * @template {API.Scalar} T
 22   * @param {API.Type<T>} type
 23   * @param {API.Scalar} value
 24   * @returns {value is T}
 25   */
 26  export const isTypeOf = (type, value) => !unify(type, infer(value)).error
 27  
 28  /**
 29   * Checks given `value` against the given `type` and returns either an ok
 30   * result or type error.
 31   *
 32   * @template {API.Scalar} T
 33   * @param {API.Type<T>} type
 34   * @param {API.Scalar} value
 35   * @returns {API.Result<API.Type, TypeError>}
 36   */
 37  export const check = (type, value) => unify(type, infer(value))
 38  
 39  /**
 40   * Attempts to unify give types and returns error if types can not be unified
 41   * or the unified type.
 42   *
 43   * @param {API.Type} type
 44   * @param {API.Type} other
 45   * @returns {API.Result<API.Type, TypeError>}
 46   */
 47  export const unify = (type, other) => {
 48    const expect = toTypeName(type)
 49    const actual = toTypeName(other)
 50    if (expect === actual) {
 51      return { ok: type }
 52    } else {
 53      return {
 54        error: new TypeError(`Expected type ${expect}, instead got ${actual}`),
 55      }
 56    }
 57  }
 58  
 59  /**
 60   * Infers the type of the given constant value. It wil throw an exception at
 61   * runtime if the value passed is not a constant type, which should not happen
 62   * if you type check the code but could if used from unchecked JS code.
 63   *
 64   * @param {API.Scalar} value
 65   * @returns {API.Type}
 66   */
 67  export const infer = (value) => {
 68    switch (typeof value) {
 69      case 'boolean':
 70        return Boolean
 71      case 'string':
 72        return String
 73      case 'bigint':
 74        return Integer
 75      case 'number':
 76        return (
 77          Number.isInteger(value) ? Integer
 78          : Number.isFinite(value) ? Float
 79          : unreachable(`Number ${value} can not be inferred`)
 80        )
 81      default: {
 82        if (value instanceof Uint8Array) {
 83          return Bytes
 84        } else if (isLink(value)) {
 85          return Referenece
 86        } else if (value === null) {
 87          return Null
 88        } else {
 89          throw Object.assign(new TypeError(`Object types are not supported`), {
 90            value,
 91          })
 92        }
 93      }
 94    }
 95  }
 96  
 97  /**
 98   * Returns JSON representation of the given type.
 99   *
100   * @template {API.Scalar} T
101   * @param {API.Type<T>} type
102   * @returns {API.Type<T>}
103   */
104  export const toJSON = (type) => /** @type {any} */ ({ [toString(type)]: {} })
105  
106  /**
107   * Returns string representation of the given type.
108   *
109   * @param {API.Type} type
110   */
111  export const toString = (type) => toTypeName(type)
112  
113  export { toJSON as inspect }
114  
115  /**
116   * Returns the discriminant of the given type.
117   *
118   * @param {API.Type} type
119   * @returns {API.TypeName}
120   */
121  export const toTypeName = (type) => {
122    if (type.Boolean) {
123      return 'boolean'
124    } else if (type.Bytes) {
125      return 'bytes'
126    } else if (type.Float) {
127      return 'float'
128    } else if (type.Integer) {
129      return 'integer'
130    } else if (type.Reference) {
131      return 'reference'
132    } else if (type.String) {
133      return 'string'
134    } else if (type.Null) {
135      return 'null'
136    } else {
137      throw new TypeError(`Invalid type ${type}`)
138    }
139  }
140  
141  /**
142   * @param {API.Type} type
143   * @returns {{[Case in keyof API.Type]: [Case, Unit, {[K in Case]: Unit}]}[keyof API.Type & string] & {}}
144   */
145  export const match = (type) => {
146    if (type.Boolean) {
147      return ['Boolean', type.Boolean, type]
148    } else if (type.Bytes) {
149      return ['Bytes', type.Bytes, type]
150    } else if (type.Float) {
151      return ['Float', type.Float, type]
152    } else if (type.Integer) {
153      return ['Integer', type.Integer, type]
154    } else if (type.Reference) {
155      return ['Reference', type.Reference, type]
156    } else if (type.String) {
157      return ['String', type.String, type]
158    } else if (type.Null) {
159      return ['Null', type.Null, type]
160    } else {
161      throw new TypeError(`Invalid type ${type}`)
162    }
163  }
164  
165  /**
166   * @param {string} message
167   * @returns {never}
168   */
169  export const unreachable = (message) => {
170    throw new Error(message)
171  }
172  
173  export const Unit = /** @type {API.Unit} */ Object.freeze({})
174  export const Null = /** @type {API.Type<API.Null>} */ ({ Null: { order: 0 } })
175  export const Boolean = /** @type {API.Type<boolean>} */ ({
176    Boolean: { order: 1 },
177  })
178  export const Integer = /** @type {API.Type<API.Integer>} */ ({
179    Integer: { order: 2 },
180  })
181  
182  export const Float = /** @type {API.Type<API.Float>} */ ({
183    Float: { order: 4 },
184  })
185  
186  export const String = /** @type {API.Type<string>} */ ({ String: { order: 5 } })
187  
188  export const Bytes = /** @type {API.Type<API.Bytes>} */ ({
189    Bytes: { order: 6 },
190  })
191  export const Referenece = /** @type {API.Type<API.Reference>} */ ({
192    Reference: { order: 9 },
193  })