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 })