Last active
February 19, 2020 14:58
-
-
Save dschnare/4c886e6a7281875e7a090403afdd6812 to your computer and use it in GitHub Desktop.
Functional runtime type checking functions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Get the type name for a value. | |
* | |
* @example type(null) // 'Null' | |
* @example type(undefined) // 'Undefined' | |
* @example type(45) // 'Number' | |
* @example type('Hello') // 'String' | |
* @example type({}) // 'Object' | |
* @example type(/hi/) // 'RegExp' | |
* @example type(new Date()) // 'Date' | |
* @example type(new Set()) // 'Set' | |
* @example type(new Map()) // 'Map' | |
* @example type([]) // 'Array' | |
*/ | |
const type = value => { | |
if (value === null) { | |
return 'Null' | |
} | |
if (value === undefined) { | |
return 'Undefined' | |
} | |
return Object.prototype.toString.call(value).slice(8, -1) | |
} | |
/** | |
* Test a value against a type expression. | |
* | |
* A type expression can be one of the following: | |
* - a type name as returned from calling type() | |
* - a constructor function | |
* - a Protocol object | |
* | |
* @example islike('Number', 45) // true | |
* @example islike('String', '45') // true | |
* @example islike('Boolean', false) // true | |
* @example islike('Boolean', {}) // false | |
* @example islike(Record({ age: 'Number' }), { age: 67 }) // true | |
*/ | |
const islike = (typeExpr, value) => { | |
switch (type(typeExpr)) { | |
case 'Protocol': | |
return !!typeExpr.test(value) | |
case 'Function': | |
return value instanceof typeExpr | |
case 'String': | |
return type(value) === typeExpr | |
default: | |
throw new TypeError('Argument "typeExpr" must be a function, string or Protocol object') | |
} | |
} | |
/** | |
* Create a protocol. A protocol is an object that performs a custom type check. | |
* | |
* @example | |
* // Create a protocol for testing a value as being a non-empty string. | |
* const Name = Protocol(value => typeof value === 'string' && value.trim()) | |
*/ | |
const Protocol = test => { | |
if (typeof test !== 'function') { | |
throw new TypeError('Argument "test" must be a function') | |
} | |
if (test.length !== 1) { | |
throw new TypeError('Argument "test" must accept only one formal argument') | |
} | |
return Object.freeze({ | |
test, | |
get [Symbol.toStringTag] () { | |
return 'Protocol' | |
} | |
}) | |
} | |
/** | |
* Wrap a type expression in a nullable protocol. | |
* | |
* @example islike(Nullable('Number'), null) // true | |
* @example islike(Nullable('Number'), undefined) // true | |
* @example islike(Nullable('Number'), 45) // true | |
* @example islike(Nullable('Number'), '45') // false | |
*/ | |
const Nullable = typeExpr => { | |
return Protocol(value => { | |
return value === null || | |
value === undefined || | |
islike(typeExpr, value) | |
}) | |
} | |
/** | |
* Create a protocol that describes properties on an object. | |
* | |
* @example islike(Record({ age: 'Number' }), { age: 45 }) // true | |
* @example islike(Record({ age: 'Number' }), {}) // false | |
*/ | |
const Record = fields => { | |
if (Object(fields) !== fields) { | |
throw new TypeError('Argument "fields" must be an object') | |
} | |
const fieldArray = Object.freeze( | |
Object.keys(fields).map(fieldName => { | |
return Object.freeze({ | |
name: fieldName, | |
type: fields[fieldName] | |
}) | |
}) | |
) | |
return Protocol(value => { | |
return value && fieldArray.every(field => { | |
return islike(field.type, value[field.name]) | |
}) | |
}) | |
} | |
/** | |
* Create a protocol that describes an iterable collection. | |
* | |
* @example islike(Collection('Number'), [1, 2, 3]) // true | |
* @example islike(Collection('Number'), [1, '2', 3]) // false | |
* @example islike(Collection('Number'), new Set([1, '2', 3])) // true | |
* @example islike(Collection('Number', { collectionType: 'Array' }), [1, '2', 3]) // true | |
* @example islike(Collection('Number', { collectionType: 'Array' }), new Set([1, '2', 3])) // false | |
*/ | |
const Collection = (itemType, { collectionType = null } = {}) => { | |
return Protocol(value => { | |
return value && | |
typeof value[Symbol.iterator] === 'function' && | |
(collectionType === null || type(value) === collectionType) && | |
[...value].every(it => islike(itemType, it)) | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment