├── .gitignore ├── nothrow.js ├── bench ├── package.json └── index.js ├── async.js ├── .github └── workflows │ └── tests.yml ├── native.js ├── package.json ├── test ├── values.js ├── types.js └── index.js ├── LICENSE ├── extra.js ├── scripts └── generate_tests.js ├── errors.js ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /nothrow.js: -------------------------------------------------------------------------------- 1 | const typeforce = require('./') 2 | 3 | function tfNoThrow (type, value, strict) { 4 | try { 5 | return typeforce(type, value, strict) 6 | } catch (e) { 7 | tfNoThrow.error = e 8 | return false 9 | } 10 | } 11 | 12 | module.exports = Object.assign(tfNoThrow, typeforce) 13 | -------------------------------------------------------------------------------- /bench/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bench", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "run.js", 6 | "scripts": { 7 | "start": "node index.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "benchmark": "^1.0.0", 13 | "typeforce": "1.11.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /async.js: -------------------------------------------------------------------------------- 1 | const typeforce = require('./') 2 | 3 | // async wrapper 4 | function tfAsync (type, value, strict, callback) { 5 | // default to falsy strict if using shorthand overload 6 | if (typeof strict === 'function') return tfAsync(type, value, false, strict) 7 | 8 | try { 9 | typeforce(type, value, strict) 10 | } catch (e) { 11 | return callback(e) 12 | } 13 | 14 | callback() 15 | } 16 | 17 | module.exports = Object.assign(tfAsync, typeforce) 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | unit: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | # see https://nodejs.org/en/about/releases/ 16 | node-version: [14.x, 16.x, 18.x] 17 | 18 | steps: 19 | - uses: actions/checkout@main 20 | - uses: actions/setup-node@main 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm install 24 | - run: npm test 25 | -------------------------------------------------------------------------------- /native.js: -------------------------------------------------------------------------------- 1 | const types = { 2 | Array: function (value) { return value !== null && value !== undefined && value.constructor === Array }, 3 | Boolean: function (value) { return typeof value === 'boolean' }, 4 | Function: function (value) { return typeof value === 'function' }, 5 | Nil: function (value) { return value === undefined || value === null }, 6 | Number: function (value) { return typeof value === 'number' }, 7 | Object: function (value) { return typeof value === 'object' }, 8 | String: function (value) { return typeof value === 'string' }, 9 | '': function () { return true } 10 | } 11 | 12 | // TODO: deprecate 13 | types.Null = types.Nil 14 | 15 | for (const typeName in types) { 16 | types[typeName].toJSON = function (t) { 17 | return t 18 | }.bind(null, typeName) 19 | } 20 | 21 | module.exports = types 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeforce", 3 | "version": "1.18.0", 4 | "description": "Another biased type checking solution for Javascript", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test/*.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/dcousens/typeforce.git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/dcousens/typeforce/issues" 15 | }, 16 | "homepage": "https://github.com/dcousens/typeforce", 17 | "keywords": [ 18 | "typeforce", 19 | "types", 20 | "typechecking", 21 | "type", 22 | "exceptions", 23 | "force" 24 | ], 25 | "author": "Daniel Cousens", 26 | "license": "MIT", 27 | "files": [ 28 | "async.js", 29 | "errors.js", 30 | "extra.js", 31 | "index.js", 32 | "native.js", 33 | "nothrow.js" 34 | ], 35 | "devDependencies": { 36 | "tape": "^5.0.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/values.js: -------------------------------------------------------------------------------- 1 | var buffer3 = Buffer.from('ffffff', 'hex') 2 | var buffer10 = Buffer.from('ffffffffffffffffffff', 'hex') 3 | 4 | module.exports = { 5 | 'function': function () {}, 6 | 'emptyType': new function EmptyType () {}(), 7 | 'customType': new function CustomType () { this.x = 2 }(), 8 | '{ a: undefined }': { a: undefined }, 9 | '{ a: Buffer3 }': { a: buffer3 }, 10 | '{ a: Buffer10 }': { a: buffer10 }, 11 | '{ a: { b: Buffer3 } }': { a: { b: buffer3 } }, 12 | '{ a: { b: Buffer10 } }': { a: { b: buffer10 } }, 13 | '{ x: 1 }': { x: 1 }, 14 | '{ y: 2 }': { y: 2 }, 15 | '{ x: 1, y: 2 }': { x: 1, y: 2 }, 16 | 'Array5': [1, 2, 3, 4, 5], 17 | 'Array6': [1, 2, 3, 4, 5, 6], 18 | 'Array7-N': [1, 2, 3, 4, 5, 6, 7], 19 | 'Array6-S': ['a', 'b', 'c', 'd', 'e', 'f'], 20 | 'Array7': ['a', 'b', 'c', 'd', 'e', 'fghijklmno', 'p'], 21 | 'Buffer': Buffer.alloc(0), 22 | 'Buffer3': buffer3, 23 | 'Buffer10': buffer10, 24 | 'String4': 'boop' 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Daniel Cousens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /extra.js: -------------------------------------------------------------------------------- 1 | var NATIVE = require('./native') 2 | var ERRORS = require('./errors') 3 | 4 | function _Buffer (value) { 5 | return Buffer.isBuffer(value) 6 | } 7 | 8 | function Hex (value) { 9 | return typeof value === 'string' && /^([0-9a-f]{2})+$/i.test(value) 10 | } 11 | 12 | function _LengthN (type, length) { 13 | var name = type.toJSON() 14 | 15 | function Length (value) { 16 | if (!type(value)) return false 17 | if (value.length === length) return true 18 | 19 | throw ERRORS.tfCustomError(name + '(Length: ' + length + ')', name + '(Length: ' + value.length + ')') 20 | } 21 | Length.toJSON = function () { return name } 22 | 23 | return Length 24 | } 25 | 26 | var _ArrayN = _LengthN.bind(null, NATIVE.Array) 27 | var _BufferN = _LengthN.bind(null, _Buffer) 28 | var _HexN = _LengthN.bind(null, Hex) 29 | var _StringN = _LengthN.bind(null, NATIVE.String) 30 | 31 | function Range (a, b, f) { 32 | f = f || NATIVE.Number 33 | function _range (value, strict) { 34 | return f(value, strict) && (value > a) && (value < b) 35 | } 36 | _range.toJSON = function () { 37 | return `${f.toJSON()} between [${a}, ${b}]` 38 | } 39 | return _range 40 | } 41 | 42 | var INT53_MAX = Math.pow(2, 53) - 1 43 | 44 | function Finite (value) { 45 | return typeof value === 'number' && isFinite(value) 46 | } 47 | function Int8 (value) { return ((value << 24) >> 24) === value } 48 | function Int16 (value) { return ((value << 16) >> 16) === value } 49 | function Int32 (value) { return (value | 0) === value } 50 | function Int53 (value) { 51 | return typeof value === 'number' && 52 | value >= -INT53_MAX && 53 | value <= INT53_MAX && 54 | Math.floor(value) === value 55 | } 56 | function UInt8 (value) { return (value & 0xff) === value } 57 | function UInt16 (value) { return (value & 0xffff) === value } 58 | function UInt32 (value) { return (value >>> 0) === value } 59 | function UInt53 (value) { 60 | return typeof value === 'number' && 61 | value >= 0 && 62 | value <= INT53_MAX && 63 | Math.floor(value) === value 64 | } 65 | 66 | var types = { 67 | ArrayN: _ArrayN, 68 | Buffer: _Buffer, 69 | BufferN: _BufferN, 70 | Finite: Finite, 71 | Hex: Hex, 72 | HexN: _HexN, 73 | Int8: Int8, 74 | Int16: Int16, 75 | Int32: Int32, 76 | Int53: Int53, 77 | Range: Range, 78 | StringN: _StringN, 79 | UInt8: UInt8, 80 | UInt16: UInt16, 81 | UInt32: UInt32, 82 | UInt53: UInt53 83 | } 84 | 85 | for (var typeName in types) { 86 | types[typeName].toJSON = function (t) { 87 | return t 88 | }.bind(null, typeName) 89 | } 90 | 91 | module.exports = types 92 | -------------------------------------------------------------------------------- /bench/index.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const benchmark = require('benchmark') 3 | const local = require('../') 4 | const npm = require('typeforce') 5 | const TYPES = require('../test/types') 6 | const VALUES = require('../test/values') 7 | const tests = require('../test/fixtures') 8 | const fixtures = tests.valid.concat(tests.invalid) 9 | 10 | fixtures.forEach(function (f) { 11 | const type = TYPES[f.typeId] || f.type 12 | const value = VALUES[f.valueId] || f.value 13 | const ctype = local.compile(type) 14 | 15 | if (f.exception) { 16 | assert.throws(function () { local(type, value, f.strict) }, new RegExp(f.exception)) 17 | // assert.throws(function () { npm(type, value, f.strict) }, new RegExp(f.exception)) 18 | assert.throws(function () { local(ctype, value, f.strict) }, new RegExp(f.exception)) 19 | // assert.throws(function () { npm(ctype, value, f.strict) }, new RegExp(f.exception)) 20 | } else { 21 | local(type, value, f.strict) 22 | npm(type, value, f.strict) 23 | local(ctype, value, f.strict) 24 | npm(ctype, value, f.strict) 25 | } 26 | }) 27 | 28 | // benchmark.options.minTime = 1 29 | fixtures.forEach(function (f) { 30 | const suite = new benchmark.Suite() 31 | const tdescription = JSON.stringify(f.type) 32 | const type = TYPES[f.typeId] || f.type 33 | const value = VALUES[f.valueId] || f.value 34 | const ctype = local.compile(type) 35 | 36 | if (f.exception) { 37 | suite.add('local(e)#' + tdescription, function () { try { local(type, value, f.strict) } catch (e) {} }) 38 | suite.add(' npm(e)#' + tdescription, function () { try { npm(type, value, f.strict) } catch (e) {} }) 39 | suite.add('local(c, e)#' + tdescription, function () { try { local(ctype, value, f.strict) } catch (e) {} }) 40 | suite.add(' npm(c, e)#' + tdescription, function () { try { npm(ctype, value, f.strict) } catch (e) {} }) 41 | } else { 42 | suite.add('local#' + tdescription, function () { local(type, value, f.strict) }) 43 | suite.add(' npm#' + tdescription, function () { npm(type, value, f.strict) }) 44 | suite.add('local(c)#' + tdescription, function () { local(ctype, value, f.strict) }) 45 | suite.add(' npm(c)#' + tdescription, function () { npm(ctype, value, f.strict) }) 46 | } 47 | 48 | // after each cycle 49 | suite.on('cycle', function (event) { 50 | console.log('*', String(event.target)) 51 | }) 52 | 53 | // other handling 54 | suite.on('complete', function () { 55 | console.log('\n> Fastest is' + (' ' + this.filter('fastest').pluck('name').join(' | ')).replace(/\s+/, ' ') + '\n') 56 | }) 57 | 58 | suite.on('error', function (event) { 59 | throw event.target.error 60 | }) 61 | 62 | suite.run() 63 | }) 64 | -------------------------------------------------------------------------------- /test/types.js: -------------------------------------------------------------------------------- 1 | var typeforce = require('../') 2 | 3 | function Unmatchable () { return false } 4 | function Letter (value) { 5 | return /^[a-z]$/i.test(value) 6 | } 7 | 8 | module.exports = { 9 | '(Boolean, Number)': typeforce.tuple('Boolean', 'Number'), 10 | '(Number|String)': typeforce.tuple(typeforce.anyOf('Number', 'String')), 11 | '(Number)': typeforce.tuple('Number'), 12 | '[?{ a: Number }]': [ typeforce.maybe({ a: 'Number' }) ], 13 | 'Boolean|Number|String': typeforce.anyOf('Boolean', 'Number', 'String'), 14 | '?Boolean|Number': typeforce.maybe(typeforce.anyOf('Boolean', 'Number')), 15 | '?{ a: ?Number }': typeforce.maybe({ a: '?Number' }), 16 | '?{ a: Number }': typeforce.maybe({ a: 'Number' }), 17 | '{ a: Number|Nil }': { a: typeforce.anyOf('Number', typeforce.Nil) }, 18 | '{ a: Number|{ b: Number } }': { a: typeforce.anyOf('Number', { b: 'Number' }) }, 19 | '{ a: ?{ b: Number } }': { a: typeforce.maybe({ b: 'Number' }) }, 20 | '{ a: ?{ b: ?{ c: Number } } }': { a: typeforce.maybe({ b: typeforce.maybe({ c: 'Number' }) }) }, 21 | '{ a: undefined }': { a: undefined }, 22 | '@{ a: undefined }': typeforce.object({ a: undefined }), // DEPRECATED 23 | 'Unmatchable': Unmatchable, 24 | '?Unmatchable': typeforce.maybe(Unmatchable), 25 | '{ a: ?Unmatchable }': { a: typeforce.maybe(Unmatchable) }, 26 | '{ a: { b: Unmatchable } }': { a: { b: Unmatchable } }, 27 | '>CustomType': typeforce.quacksLike('CustomType'), 28 | '{ String }': typeforce.map('String'), 29 | '{ String|Number }': typeforce.map(typeforce.anyOf('String', 'Number')), 30 | '{ String: Number }': typeforce.map('Number', 'String'), 31 | '{ Letter: Number }': typeforce.map('Number', Letter), 32 | '{ a: { b: Buffer3 } }': { a: { b: typeforce.BufferN(3) } }, 33 | '{ a: Buffer10|Number }': { a: typeforce.anyOf(typeforce.BufferN(10), 'Number') }, 34 | '{ a: { b: Buffer } }': typeforce.allOf({ a: typeforce.Object }, { a: { b: typeforce.Buffer } }), 35 | '{ x: Number } & { y: Number }': typeforce.allOf({ x: typeforce.Number }, { y: typeforce.Number }), 36 | '{ x: Number } & { z: Number }': typeforce.allOf({ x: typeforce.Number }, { z: typeforce.Number }), 37 | 'Array6(Number)': typeforce.arrayOf(typeforce.Number, { length: 6 }), 38 | 'Array>=6(Number)': typeforce.arrayOf(typeforce.Number, { minLength: 6 }), 39 | 'Array<=6(Number)': typeforce.arrayOf(typeforce.Number, { maxLength: 6 }), 40 | 'Array6': typeforce.ArrayN(6), 41 | 'Array7': typeforce.ArrayN(7), 42 | 'Buffer0': typeforce.BufferN(0), 43 | 'Buffer3': typeforce.BufferN(3), 44 | 'Buffer10': typeforce.BufferN(10), 45 | 'Hex': typeforce.Hex, 46 | 'Hex64': typeforce.HexN(64), 47 | 'String4': typeforce.StringN(4), 48 | 'Range1-5': typeforce.Range(1, 5), 49 | 'Int8Range0-100': typeforce.Range(0, 100, typeforce.Int8), 50 | 'Int8': typeforce.Int8, 51 | 'Int16': typeforce.Int16, 52 | 'Int32': typeforce.Int32, 53 | 'Int53': typeforce.Int53, 54 | 'UInt8': typeforce.UInt8, 55 | 'UInt16': typeforce.UInt16, 56 | 'UInt32': typeforce.UInt32, 57 | 'UInt53': typeforce.UInt53 58 | } 59 | -------------------------------------------------------------------------------- /scripts/generate_tests.js: -------------------------------------------------------------------------------- 1 | #!/bin/node 2 | const typeforce = require('../') 3 | const TYPES = require('../test/types') 4 | const VALUES = require('../test/values') 5 | 6 | const TYPES2 = [ 7 | 'Array', 8 | 'Boolean', 9 | 'Buffer', 10 | 'Function', 11 | 'Null', 12 | 'Number', 13 | 'Object', 14 | 'String', 15 | '?Number', 16 | [ '?Number' ], 17 | [ 'Number' ], 18 | [ { a: 'Number' } ], 19 | {}, 20 | { a: 'Number' }, 21 | { a: { b: 'Number' } }, 22 | { a: { b: { c: '?Number' } } }, 23 | { a: { b: { c: 'Number' } } }, 24 | { a: null }, 25 | 26 | // these will resolve to typeforce.value(...) 27 | undefined, 28 | null, 29 | true, 30 | false, 31 | 0 32 | ] 33 | 34 | const VALUES2 = [ 35 | '', 36 | 'foobar', 37 | 0, 38 | 1, 39 | 1.5, 40 | 10, 41 | [], 42 | [0], 43 | ['foobar'], 44 | [{ a: 0 }], 45 | [null], 46 | false, 47 | true, 48 | undefined, 49 | null, 50 | {}, 51 | { a: null }, 52 | { a: 0 }, 53 | { a: 0, b: 0 }, 54 | { b: 0 }, 55 | { a: { b: 0 } }, 56 | { a: { b: null } }, 57 | { a: { b: { c: 0 } } }, 58 | { a: { b: { c: null } } }, 59 | { a: { b: { c: 0, d: 0 } } }, 60 | { a: 'foo', b: 'bar' }, 61 | { a: 'foo', b: { c: 'bar' } } 62 | ] 63 | 64 | const INT53_MAX = Math.pow(2, 53) - 1 65 | 66 | // extra 67 | const VALUESX = [ 68 | 'fff', 69 | 'cafe1122deadbeef', 70 | '0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20', 71 | -1, 72 | 127, 73 | 128, 74 | 255, 75 | 256, 76 | -128, 77 | -129, 78 | 0xfffe, 79 | 0xffff, 80 | 0x10000, 81 | 0xffffffff, 82 | INT53_MAX, 83 | INT53_MAX + 3, 84 | -INT53_MAX, 85 | -INT53_MAX - 3 86 | ] 87 | 88 | const fixtures = { 89 | valid: [], 90 | invalid: [] 91 | } 92 | 93 | function addFixture (type, value) { 94 | const f = {} 95 | let atype, avalue 96 | 97 | if (TYPES[type]) { 98 | f.typeId = type 99 | atype = TYPES[type] 100 | } else { 101 | f.type = type 102 | atype = type 103 | } 104 | 105 | if (VALUES[value]) { 106 | f.valueId = value 107 | avalue = VALUES[value] 108 | } else { 109 | f.value = value 110 | avalue = value 111 | } 112 | 113 | try { 114 | typeforce(atype, avalue, true) 115 | fixtures.valid.push(f) 116 | } catch (e) { 117 | let message = e.message 118 | .replace(/([.*+?^=!:${}[\]/\\()])/g, '\\$&') 119 | 120 | try { 121 | typeforce(atype, avalue, false) 122 | fixtures.valid.push(f) 123 | 124 | if (message.indexOf('asciiSlice') !== -1) return 125 | fixtures.invalid.push(Object.assign({ exception: message, strict: true }, f)) 126 | } catch (e2) { 127 | message = e2.message 128 | .replace(/([.*+?^=!:${}[\]/\\()])/g, '\\$&') 129 | 130 | if (message.indexOf('asciiSlice') !== -1) return 131 | fixtures.invalid.push(Object.assign({ exception: message }, f)) 132 | } 133 | } 134 | } 135 | 136 | const ALLTYPES = TYPES2.concat(Object.keys(TYPES)) 137 | const ALLVALUES = VALUES2.concat(Object.keys(VALUES)) 138 | 139 | ALLTYPES.forEach(type => ALLVALUES.forEach(value => addFixture(type, value))) 140 | ALLTYPES.forEach(type => VALUESX.forEach(value => addFixture(type, value))) 141 | 142 | console.log(JSON.stringify(fixtures, null, 2)) 143 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape') 2 | const typeforce = require('../') 3 | const typeforceAsync = require('../async') 4 | const typeforceNoThrow = require('../nothrow') 5 | const fixtures = require('./fixtures') 6 | const TYPES = require('./types') 7 | const VALUES = require('./values') 8 | 9 | for (const f of fixtures.valid) { 10 | const type = TYPES[f.typeId] || f.type 11 | const value = VALUES[f.valueId] || f.value 12 | const typeDescription = JSON.stringify(type) 13 | const valueDescription = JSON.stringify(value) 14 | const compiled = typeforce.compile(type) 15 | 16 | tape('passes ' + typeDescription + ' with ' + valueDescription, function (t) { 17 | t.plan(6) 18 | t.doesNotThrow(function () { typeforce(type, value, f.strict) }) 19 | typeforceAsync(type, value, f.strict, t.ifErr) 20 | t.equal(typeforceNoThrow(type, value, f.strict), true) 21 | 22 | t.doesNotThrow(function () { typeforce(compiled, value, f.strict) }) 23 | typeforceAsync(compiled, value, f.strict, t.ifErr) 24 | t.equal(typeforceNoThrow(compiled, value, f.strict), true) 25 | }) 26 | } 27 | 28 | for (const f of fixtures.invalid) { 29 | if (!f.exception) throw new TypeError('Expected exception') 30 | 31 | const type = TYPES[f.typeId] || f.type 32 | const value = VALUES[f.valueId] || f.value 33 | const typeDescription = f.typeId || JSON.stringify(type) 34 | const valueDescription = JSON.stringify(value) 35 | const compiled = typeforce.compile(type) 36 | 37 | tape('throws "' + f.exception + '" for type ' + typeDescription + ' with value of ' + valueDescription, function (t) { 38 | t.plan(10) 39 | 40 | t.throws(function () { 41 | typeforce(type, value, f.strict) 42 | }, new RegExp(f.exception)) 43 | typeforceAsync(type, value, f.strict, (err) => { 44 | t.ok(err) 45 | t.throws(function () { throw err }, new RegExp(f.exception)) 46 | }) 47 | t.equal(typeforceNoThrow(type, value, f.strict), false) 48 | t.throws(function () { throw typeforceNoThrow.error }, new RegExp(f.exception)) 49 | 50 | t.throws(function () { 51 | typeforce(compiled, value, f.strict) 52 | }, new RegExp(f.exception)) 53 | typeforceAsync(compiled, value, f.strict, (err) => { 54 | t.ok(err) 55 | t.throws(function () { throw err }, new RegExp(f.exception)) 56 | }) 57 | t.equal(typeforceNoThrow(compiled, value, f.strict), false) 58 | t.throws(function () { throw typeforceNoThrow.error }, new RegExp(f.exception)) 59 | }) 60 | } 61 | 62 | const err = new typeforce.TfTypeError('mytype') 63 | function failType () { throw err } 64 | 65 | tape('TfTypeError is an Error', function (t) { 66 | t.plan(3) 67 | t.ok(err instanceof Error) 68 | t.equal(err.message, 'Expected mytype, got undefined') 69 | 70 | t.throws(function () { 71 | typeforce(failType, 0xdeadbeef) 72 | }, new RegExp('Expected mytype, got undefined')) 73 | }) 74 | 75 | tape('TfTypeError is caught by typeforce.oneOf', function (t) { 76 | t.plan(2) 77 | 78 | t.doesNotThrow(function () { 79 | typeforce.oneOf(failType)('value') 80 | }) 81 | 82 | t.ok(!typeforce.oneOf(failType, typeforce.string)('value')) 83 | }) 84 | 85 | tape('Error is thrown for bad compile parameters', function (t) { 86 | t.plan(2) 87 | 88 | t.throws(function () { 89 | typeforce.compile([]) 90 | }, /Expected compile\(\) parameter of type Array of length 1/) 91 | 92 | t.throws(function () { 93 | typeforce.compile([typeforce.Number, typeforce.String]) 94 | }, /Expected compile\(\) parameter of type Array of length 1/) 95 | }) 96 | -------------------------------------------------------------------------------- /errors.js: -------------------------------------------------------------------------------- 1 | const native = require('./native') 2 | 3 | function getTypeName (fn) { 4 | return fn.name || fn.toString().match(/function (.*?)\s*\(/)[1] 5 | } 6 | 7 | function getValueTypeName (value) { 8 | return native.Nil(value) ? '' : getTypeName(value.constructor) 9 | } 10 | 11 | function getValue (value) { 12 | if (native.Function(value)) return '' 13 | if (native.String(value)) return JSON.stringify(value) 14 | if (value && native.Object(value)) return '' 15 | return value 16 | } 17 | 18 | function captureStackTrace (e, t) { 19 | if (Error.captureStackTrace) { 20 | Error.captureStackTrace(e, t) 21 | } 22 | } 23 | 24 | function tfJSON (type) { 25 | if (native.Function(type)) return type.toJSON ? type.toJSON() : getTypeName(type) 26 | if (native.Array(type)) return 'Array' 27 | if (type && native.Object(type)) return 'Object' 28 | 29 | return type !== undefined ? type : '' 30 | } 31 | 32 | function tfErrorString (type, value, valueTypeName) { 33 | const valueJson = getValue(value) 34 | 35 | return 'Expected ' + tfJSON(type) + ', got' + 36 | (valueTypeName !== '' ? ' ' + valueTypeName : '') + 37 | (valueJson !== '' ? ' ' + valueJson : '') 38 | } 39 | 40 | function TfTypeError (type, value, valueTypeName) { 41 | valueTypeName = valueTypeName || getValueTypeName(value) 42 | this.message = tfErrorString(type, value, valueTypeName) 43 | 44 | captureStackTrace(this, TfTypeError) 45 | this.__type = type 46 | this.__value = value 47 | this.__valueTypeName = valueTypeName 48 | } 49 | 50 | TfTypeError.prototype = Object.create(Error.prototype) 51 | TfTypeError.prototype.constructor = TfTypeError 52 | 53 | function tfPropertyErrorString (type, label, name, value, valueTypeName) { 54 | let description = '" of type ' 55 | if (label === 'key') description = '" with key type ' 56 | 57 | return tfErrorString('property "' + tfJSON(name) + description + tfJSON(type), value, valueTypeName) 58 | } 59 | 60 | function TfPropertyTypeError (type, property, label, value, valueTypeName) { 61 | if (type) { 62 | valueTypeName = valueTypeName || getValueTypeName(value) 63 | this.message = tfPropertyErrorString(type, label, property, value, valueTypeName) 64 | } else { 65 | this.message = 'Unexpected property "' + property + '"' 66 | } 67 | 68 | captureStackTrace(this, TfTypeError) 69 | this.__label = label 70 | this.__property = property 71 | this.__type = type 72 | this.__value = value 73 | this.__valueTypeName = valueTypeName 74 | } 75 | 76 | TfPropertyTypeError.prototype = Object.create(Error.prototype) 77 | TfPropertyTypeError.prototype.constructor = TfTypeError 78 | 79 | function tfCustomError (expected, actual) { 80 | return new TfTypeError(expected, {}, actual) 81 | } 82 | 83 | function tfSubError (e, property, label) { 84 | // sub child? 85 | if (e instanceof TfPropertyTypeError) { 86 | property = property + '.' + e.__property 87 | 88 | e = new TfPropertyTypeError( 89 | e.__type, property, e.__label, e.__value, e.__valueTypeName 90 | ) 91 | 92 | // child? 93 | } else if (e instanceof TfTypeError) { 94 | e = new TfPropertyTypeError( 95 | e.__type, property, label, e.__value, e.__valueTypeName 96 | ) 97 | } 98 | 99 | captureStackTrace(e) 100 | return e 101 | } 102 | 103 | module.exports = { 104 | TfTypeError: TfTypeError, 105 | TfPropertyTypeError: TfPropertyTypeError, 106 | tfCustomError: tfCustomError, 107 | tfSubError: tfSubError, 108 | tfJSON: tfJSON, 109 | getValueTypeName: getValueTypeName 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typeforce 2 | [![Version](https://img.shields.io/npm/v/typeforce.svg)](https://www.npmjs.org/package/typeforce) 3 | 4 | Another biased type checking solution for Javascript. 5 | 6 | Exception messages may change between patch versions, as often the patch will change some behaviour that was unexpected and naturally it results in a different error message. 7 | 8 | ## Examples 9 | 10 | ``` javascript 11 | const typeforce = require('typeforce') 12 | 13 | // supported primitives 'Array', 'Boolean', 'Buffer', 'Number', 'Object', 'String' 14 | typeforce('Array', []) 15 | 16 | typeforce('Number', []) 17 | // TypeError: Expected Number, got Array 18 | 19 | // array types 20 | typeforce(['Object'], [{}]) 21 | typeforce(typeforce.arrayOf('Object'), [{}, {}, {}]) 22 | 23 | // enforces object properties 24 | typeforce({ 25 | foo: 'Number' 26 | }, { 27 | foo: 'bar' 28 | }) 29 | // TypeError: Expected property "foo" of type Number, got String "bar" 30 | 31 | // maybe types 32 | typeforce('?Number', 2) 33 | typeforce('?Number', null) 34 | typeforce(typeforce.maybe(typeforce.Number), 2) 35 | typeforce(typeforce.maybe(typeforce.Number), null) 36 | 37 | // sum types 38 | typeforce(typeforce.anyOf('String', 'Number'), 2) 39 | typeforce(typeforce.allOf({ x: typeforce.Number }, { y: typeforce.Number }), { 40 | x: 1, 41 | y: 2 42 | }) 43 | 44 | // value types 45 | typeforce(typeforce.value(3.14), 3.14) 46 | 47 | // custom types 48 | function LongString (value, strict) { 49 | if (!typeforce.String(value)) return false 50 | if (value.length !== 32) return false 51 | return true 52 | } 53 | 54 | typeforce(LongString, '00000000000000000000000000000000') 55 | // => OK! 56 | 57 | typeforce(LongString, 'not long enough') 58 | // TypeError: Expected LongString, got String 'not long enough' 59 | ``` 60 | 61 | **Pro**tips: 62 | ``` javascript 63 | // use precompiled primitives for high performance 64 | typeforce(typeforce.Array, array) 65 | 66 | // or just precompile a template 67 | const type = { 68 | foo: 'Number', 69 | bar: '?String' 70 | } 71 | 72 | const fastType = typeforce.compile(type) 73 | // fastType => typeforce.object({ 74 | // foo: typeforce.Number, 75 | // bar: typeforce.maybe(typeforce.String) 76 | // }) 77 | 78 | // use strictness for recursive types to enforce whitelisting properties 79 | typeforce({ 80 | x: 'Number' 81 | }, { x: 1 }, true) 82 | // OK! 83 | 84 | typeforce({ 85 | x: 'Number' 86 | }, { x: 1, y: 2 }, true) 87 | // TypeError: Unexpected property 'y' of type Number 88 | ``` 89 | 90 | **Pro**tips (extended types): 91 | ``` javascript 92 | typeforce(typeforce.tuple('String', 'Number'), ['foo', 1]) 93 | // OK! 94 | 95 | typeforce(typeforce.tuple('Number', 'Number'), ['not a number', 1]) 96 | // TypeError: Expected property "0" of type Number, got String 'not a number' 97 | 98 | typeforce(typeforce.map('Number'), { 99 | 'anyKeyIsOK': 1 100 | }) 101 | // OK! 102 | 103 | typeforce(typeforce.map('Number', typeforce.HexN(8)), { 104 | 'deadbeef': 1, 105 | 'ffff0000': 2 106 | }) 107 | // OK! 108 | 109 | function Foo () { 110 | this.x = 2 111 | } 112 | 113 | typeforce(typeforce.quacksLike('Foo'), new Foo()) 114 | // OK! 115 | 116 | // Note, any Foo will do 117 | typeforce(typeforce.quacksLike('Foo'), new (function Foo() {})) 118 | // OK! 119 | ``` 120 | 121 | **Pro**tips (no throw) 122 | ``` javascript 123 | const typeforce = require('typeforce/nothrow') 124 | const value = 'foobar' 125 | 126 | if (typeforce(typeforce.Number, value)) { 127 | // didn't throw! 128 | console.log(`${value} is a number`) // never happens 129 | } else { 130 | console.log(`Oops, ${typeforce.error.message}`) 131 | // prints 'Oops, Expected Number, got String foobar' 132 | } 133 | ``` 134 | 135 | **Pro**tips (async) 136 | ``` javascript 137 | const typeforce = require('typeforce/async') 138 | 139 | typeforce(typeforce.Number, value, function (err) { 140 | if (err) return console.log(`Oops, ${typeforce.error.message}`) 141 | 142 | console.log(`${value} is a number`) // never happens 143 | }) 144 | ``` 145 | 146 | **WARNING**: Be very wary of using the `quacksLike` type, as it relies on the `Foo.name` property. 147 | If that property is mangled by a transpiler, such as `uglifyjs`, you will have a bad time. 148 | 149 | ## LICENSE [MIT](LICENSE) 150 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const ERRORS = require('./errors') 2 | const NATIVE = require('./native') 3 | 4 | // short-hand 5 | const tfJSON = ERRORS.tfJSON 6 | const TfTypeError = ERRORS.TfTypeError 7 | const TfPropertyTypeError = ERRORS.TfPropertyTypeError 8 | const tfSubError = ERRORS.tfSubError 9 | const getValueTypeName = ERRORS.getValueTypeName 10 | 11 | const TYPES = { 12 | arrayOf: function arrayOf (type, options) { 13 | type = compile(type) 14 | options = options || {} 15 | 16 | function _arrayOf (array, strict) { 17 | if (!NATIVE.Array(array)) return false 18 | if (NATIVE.Nil(array)) return false 19 | if (options.minLength !== undefined && array.length < options.minLength) return false 20 | if (options.maxLength !== undefined && array.length > options.maxLength) return false 21 | if (options.length !== undefined && array.length !== options.length) return false 22 | 23 | return array.every(function (value, i) { 24 | try { 25 | return typeforce(type, value, strict) 26 | } catch (e) { 27 | throw tfSubError(e, i) 28 | } 29 | }) 30 | } 31 | _arrayOf.toJSON = function () { 32 | let str = '[' + tfJSON(type) + ']' 33 | if (options.length !== undefined) { 34 | str += '{' + options.length + '}' 35 | } else if (options.minLength !== undefined || options.maxLength !== undefined) { 36 | str += '{' + 37 | (options.minLength === undefined ? 0 : options.minLength) + ',' + 38 | (options.maxLength === undefined ? Infinity : options.maxLength) + '}' 39 | } 40 | return str 41 | } 42 | 43 | return _arrayOf 44 | }, 45 | 46 | maybe: function maybe (type) { 47 | type = compile(type) 48 | 49 | function _maybe (value, strict) { 50 | return NATIVE.Nil(value) || type(value, strict, maybe) 51 | } 52 | _maybe.toJSON = function () { return '?' + tfJSON(type) } 53 | 54 | return _maybe 55 | }, 56 | 57 | map: function map (propertyType, propertyKeyType) { 58 | propertyType = compile(propertyType) 59 | if (propertyKeyType) propertyKeyType = compile(propertyKeyType) 60 | 61 | function _map (value, strict) { 62 | if (!NATIVE.Object(value)) return false 63 | if (NATIVE.Nil(value)) return false 64 | 65 | for (const propertyName in value) { 66 | try { 67 | if (propertyKeyType) { 68 | typeforce(propertyKeyType, propertyName, strict) 69 | } 70 | } catch (e) { 71 | throw tfSubError(e, propertyName, 'key') 72 | } 73 | 74 | try { 75 | const propertyValue = value[propertyName] 76 | typeforce(propertyType, propertyValue, strict) 77 | } catch (e) { 78 | throw tfSubError(e, propertyName) 79 | } 80 | } 81 | 82 | return true 83 | } 84 | 85 | if (propertyKeyType) { 86 | _map.toJSON = function () { 87 | return '{' + tfJSON(propertyKeyType) + ': ' + tfJSON(propertyType) + '}' 88 | } 89 | } else { 90 | _map.toJSON = function () { return '{' + tfJSON(propertyType) + '}' } 91 | } 92 | 93 | return _map 94 | }, 95 | 96 | object: function object (uncompiled) { 97 | const type = {} 98 | 99 | for (const typePropertyName in uncompiled) { 100 | type[typePropertyName] = compile(uncompiled[typePropertyName]) 101 | } 102 | 103 | function _object (value, strict) { 104 | if (!NATIVE.Object(value)) return false 105 | if (NATIVE.Nil(value)) return false 106 | 107 | let propertyName 108 | 109 | try { 110 | for (propertyName in type) { 111 | const propertyType = type[propertyName] 112 | const propertyValue = value[propertyName] 113 | 114 | typeforce(propertyType, propertyValue, strict) 115 | } 116 | } catch (e) { 117 | throw tfSubError(e, propertyName) 118 | } 119 | 120 | if (strict) { 121 | for (propertyName in value) { 122 | if (type[propertyName]) continue 123 | 124 | throw new TfPropertyTypeError(undefined, propertyName) 125 | } 126 | } 127 | 128 | return true 129 | } 130 | _object.toJSON = function () { return tfJSON(type) } 131 | 132 | return _object 133 | }, 134 | 135 | anyOf: function anyOf () { 136 | const types = [].slice.call(arguments).map(compile) 137 | 138 | function _anyOf (value, strict) { 139 | return types.some(function (type) { 140 | try { 141 | return typeforce(type, value, strict) 142 | } catch (e) { 143 | return false 144 | } 145 | }) 146 | } 147 | _anyOf.toJSON = function () { return types.map(tfJSON).join('|') } 148 | 149 | return _anyOf 150 | }, 151 | 152 | allOf: function allOf () { 153 | const types = [].slice.call(arguments).map(compile) 154 | 155 | function _allOf (value, strict) { 156 | return types.every(function (type) { 157 | try { 158 | return typeforce(type, value, strict) 159 | } catch (e) { 160 | return false 161 | } 162 | }) 163 | } 164 | _allOf.toJSON = function () { return types.map(tfJSON).join(' & ') } 165 | 166 | return _allOf 167 | }, 168 | 169 | quacksLike: function quacksLike (type) { 170 | function _quacksLike (value) { 171 | return type === getValueTypeName(value) 172 | } 173 | _quacksLike.toJSON = function () { return type } 174 | 175 | return _quacksLike 176 | }, 177 | 178 | tuple: function tuple () { 179 | const types = [].slice.call(arguments).map(compile) 180 | 181 | function _tuple (values, strict) { 182 | if (NATIVE.Nil(values)) return false 183 | if (NATIVE.Nil(values.length)) return false 184 | if (strict && (values.length !== types.length)) return false 185 | 186 | return types.every(function (type, i) { 187 | try { 188 | return typeforce(type, values[i], strict) 189 | } catch (e) { 190 | throw tfSubError(e, i) 191 | } 192 | }) 193 | } 194 | _tuple.toJSON = function () { return '(' + types.map(tfJSON).join(', ') + ')' } 195 | 196 | return _tuple 197 | }, 198 | 199 | value: function value (expected) { 200 | function _value (actual) { 201 | return actual === expected 202 | } 203 | _value.toJSON = function () { return expected } 204 | 205 | return _value 206 | } 207 | } 208 | 209 | // TODO: deprecate 210 | TYPES.oneOf = TYPES.anyOf 211 | 212 | function compile (type) { 213 | if (NATIVE.String(type)) { 214 | if (type[0] === '?') return TYPES.maybe(type.slice(1)) 215 | 216 | return NATIVE[type] || TYPES.quacksLike(type) 217 | } else if (type && NATIVE.Object(type)) { 218 | if (NATIVE.Array(type)) { 219 | if (type.length !== 1) throw new TypeError('Expected compile() parameter of type Array of length 1') 220 | return TYPES.arrayOf(type[0]) 221 | } 222 | 223 | return TYPES.object(type) 224 | } else if (NATIVE.Function(type)) { 225 | return type 226 | } 227 | 228 | return TYPES.value(type) 229 | } 230 | 231 | function typeforce (type, value, strict, surrogate) { 232 | if (NATIVE.Function(type)) { 233 | if (type(value, strict)) return true 234 | 235 | throw new TfTypeError(surrogate || type, value) 236 | } 237 | 238 | // JIT 239 | return typeforce(compile(type), value, strict) 240 | } 241 | 242 | // assign types to typeforce function 243 | for (const typeName in NATIVE) { 244 | typeforce[typeName] = NATIVE[typeName] 245 | } 246 | 247 | for (typeName in TYPES) { 248 | typeforce[typeName] = TYPES[typeName] 249 | } 250 | 251 | const EXTRA = require('./extra') 252 | for (typeName in EXTRA) { 253 | typeforce[typeName] = EXTRA[typeName] 254 | } 255 | 256 | typeforce.compile = compile 257 | typeforce.TfTypeError = TfTypeError 258 | typeforce.TfPropertyTypeError = TfPropertyTypeError 259 | 260 | module.exports = typeforce 261 | --------------------------------------------------------------------------------