Skip to content

Instantly share code, notes, and snippets.

@dschnare
Created September 2, 2019 20:43
Show Gist options
  • Save dschnare/606ee0a4e819abce76a8304f60e13da7 to your computer and use it in GitHub Desktop.
Save dschnare/606ee0a4e819abce76a8304f60e13da7 to your computer and use it in GitHub Desktop.
Type checking at runtime
/**
* Checks the type of a value. If the value is invalid then throws.
*
* Errors:
*
* TypeError
*
* Thrown whenever a value has an invalid type
*
* Properties:
*
* - `propertyName` The property name of the value being tested
* - `propertyValue` The value being tested (for convenience named as propertyName)
* - `value` The value being tested
* - `relatedErrors` An array of related TypeError's
*
* @example
* T(4, 'number') // primitive type
* T([4], ['number']) // array-of numbers
* T([[4]], ['array']) // array-of arrays
* T([4], 'array') // test if value is an array
* T([4], 'object') // test if value is an object
* T([4], Array) // test if value is a constructor instance
* T({ key: 'one', value: 1 }, { key: 'string', value: 'number' }) // object keys check
* T(['Darren', 37], T.tuple([ 'string', 'number' ])) // tuple check
* T(new Set(['One','Two']), T.iterable(Set, 'string'))) // set-of strings
* T(new Map([ ['One',1] ]), T.iterable(Map, T.tuple(['string', 'number']))) // map-of string:number entries
* @param {object} value
* @param {Function|string|object} type
* @param {string} [message]
*/
function T (value, type, message = 'Value has an invalid type') {
const isObject = () => Object(value) === value && Object(type) === type
const isArray = () => Array.isArray(value) && Array.isArray(type) && type.length === 1
const isTuple = () => Array.isArray(value) && type && type.__tuple
const isIterable = () => typeof Symbol === 'function' &&
value &&
typeof value[Symbol.iterator] === 'function' &&
type &&
type.__iterable &&
Object(value) === value &&
typeof type.type === 'function' &&
value instanceof type.type
if (type === 'array' && Array.isArray(value)) {
return
} else if (type === 'object' && Object(value) === value) {
return
} else if (typeof value === type) {
return
} else if (Object(value) === value && typeof type === 'function' && value instanceof type) {
return
} else if (isTuple()) {
const errors = type.values.map((type, index) => {
try {
T(value[index], type)
} catch (error) {
return Object.assign(
error,
{ propertyName: index, propertyValue: value[index] }
)
}
}).filter(Boolean)
if (errors.length || value.length !== type.values.length) {
throw Object.assign(new TypeError(message), { value, relatedErrors: errors })
}
} else if (isIterable()) {
let errors = []
for (let item of value) {
try {
T(item, type.itemType, message)
} catch (error) {
errors.push(error)
}
}
if (errors.length) {
throw Object.assign(new TypeError(message), { value, relatedErrors: errors })
}
} else if (isArray()) {
const errors = value.map((item, index) => {
try {
T(item, type[0])
} catch (error) {
return Object.assign(
error,
{ propertyName: index, propertyValue: value[index] }
)
}
}).filter(Boolean)
if (errors.length) {
throw Object.assign(new TypeError(message), { value, relatedErrors: errors })
}
} else if (isObject()) {
const errors = Object.keys(type).map(prop => {
try {
T(value[prop], type[prop])
} catch (error) {
return Object.assign(
error,
{ propertyName: prop, propertyValue: value[prop] }
)
}
}).filter(Boolean)
if (errors.length) {
throw Object.assign(new TypeError(message), { value, relatedErrors: errors })
}
} else {
throw Object.assign(new TypeError(message), { value })
}
}
T.tuple = function tuple (values) {
return { __tuple: true, values }
}
T.iterable = function iterable (type, itemType) {
if (typeof Symbol !== 'function') {
throw new Error('Iterables are not supported by the runtime')
}
return { __iterable: true, type, itemType }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment