Last active
February 15, 2025 11:26
-
-
Save nicholaswmin/55e9e96523bac78cad79cb041b3d62e7 to your computer and use it in GitHub Desktop.
type-validator function for primitives, null & array. Includes unit-tests and docs
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
/** @function | |
* @name validated | |
* @summary validate the type of a value | |
* | |
* @description | |
* validates a value against a {spec}. object, specifying: | |
* - parameter {name} | |
* - expected {type} | |
* {type} suppers & differentiates non-standard "array" and "null" types. | |
* | |
* @param {any} v - value to validate. | |
* @param {object} spec - object with 1 key/value pair, where: | |
* @param {string} spec.key - The expected parameter type | |
* @param {string} spec.value - The parameter name | |
* | |
* @returns {any} passed value, if it passes validation. | |
* | |
* @throws {TypeError} "ttl must be a number, got: string" if validation fails. | |
* @throws {TypeError} if spec is not an object. | |
* @throws {RangeError} if spec does not have exactly 1 key/value pair. | |
* | |
* @example | |
* const ttl = validated(300, { ttl: 'number' }) | |
* // logs: 300 | |
* | |
* @example | |
* const ttl = validated('300', { ttl: 'number' }) | |
* // throws: "ttl must be a number, got: string" | |
* | |
* @todo theres more types to validate, like: "function", "symbol", "bigint" | |
* @todo compat. with node.js util.inspect() for error highlighting. | |
*/ | |
const validated = (val, spec = {}) => { | |
const all = ['string', 'number', 'boolean', 'object', 'array', 'null'], | |
kys = Object.keys(spec), | |
len = kys.length, | |
nme = kys.at(0), | |
exp = Object.values(spec).at(0)?.toString?.(), | |
act = val === null | |
? 'null' : Array.isArray(val) | |
? 'array' : typeof val | |
if (len !== 1) | |
throw RangeError(`spec. must have: 1 K/V, has: ${len}`) | |
if (typeof spec !== 'object') | |
throw TypeError('spec. must be an object') | |
const err = typeof exp !== 'string' || !all.includes(exp) | |
? `spec {:v} can be: ${all.join(', ')}, got: '${exp}'` | |
: act !== exp ? `'${nme}' must be: ${exp}, got: ${act}` : false | |
if (err) throw new TypeError(err) | |
return val | |
} |
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
/* | |
Unit tests | |
Run: | |
```bash | |
node --test \ | |
--experimental-test-coverage \ | |
--test-coverage-lines=97 \ | |
--test-coverage-branches=97 \ | |
--test-coverage-functions=97 \ | |
validated.test.js | |
``` | |
uses node.js test runner | |
requires: node `v22+` | |
*/ | |
import { test } from 'node:test' | |
test('#validated() invalid setup', async t => { | |
await t.test('all params are missing', async t => { | |
await t.test('throws descriptive RangeError', t => { | |
t.assert.throws(() => validated(300), { | |
name: 'RangeError', | |
message: /spec. must have/ | |
}) | |
}) | |
}) | |
await t.test('spec. argument is missing', async t => { | |
await t.test('throws descriptive RangeError', t => { | |
t.assert.throws(() => validated(300), { | |
name: 'RangeError', | |
message: /spec. must have/ | |
}) | |
}) | |
}) | |
await t.test('spec. argument is not an object', async t => { | |
await t.test('throws descriptive RangeError', t => { | |
t.assert.throws(() => validated(300, 'fubar'), { | |
name: 'RangeError', | |
message: /spec. must have/ | |
}) | |
}) | |
}) | |
await t.test('spec. argument has > 1 K/V pairs', async t => { | |
await t.test('throws descriptive RangeError', t => { | |
t.assert.throws(() => validated(300, { ttl: 'string', ping: 'number' }), { | |
name: 'RangeError', | |
message: /spec. must have/ | |
}) | |
}) | |
}) | |
await t.test('spec. argument has 0 K/V pairs', async t => { | |
await t.test('throws descriptive RangeError', t => { | |
t.assert.throws(() => validated(300, {}), { | |
name: 'RangeError', | |
message: /spec. must have/ | |
}) | |
}) | |
}) | |
await t.test('spec. object defines a non-allowed type', async t => { | |
await t.test('throws descriptive RangeError', t => { | |
t.assert.throws(() => validated(300, { ttl: 'fubared' }), { | |
name: 'TypeError', | |
message: /spec {:v} can be: string, number,/ | |
}) | |
}) | |
}) | |
}) | |
test('#validated() validations', async t => { | |
await t.test('value has unexpected type', async t => { | |
await t.test('throws descriptive TypeError', t => { | |
t.assert.throws(() => validated(300, { ttl: 'string' }), { | |
name: 'TypeError', | |
message: /'ttl' must be/ | |
}) | |
}) | |
}) | |
await t.test('value is & expects a primitive type', async t => { | |
await t.test('value has unexpected type', async t => { | |
await t.test('throws descriptive TypeError', t => { | |
t.assert.throws(() => validated(300, { ttl: 'string' }), { | |
name: 'TypeError', | |
message: /'ttl' must be/ | |
}) | |
}) | |
}) | |
await t.test('value has expected type', async t => { | |
await t.test('returns the value', t => { | |
t.assert.strictEqual(validated('foo', { ttl: 'string' }), 'foo') | |
}) | |
}) | |
}) | |
await t.test('value is/expects a composite type', async t => { | |
await t.test('value has unexpected type', async t => { | |
await t.test('throws descriptive TypeError', t => { | |
t.assert.throws(() => validated({}, { ttl: 'array' }), { | |
name: 'TypeError', | |
message: /'ttl' must be/ | |
}) | |
}) | |
}) | |
await t.test('value has expected type', async t => { | |
await t.test('returns the value', t => { | |
t.assert.deepStrictEqual(validated([1, '2'], { ttl: 'array' }), [1, '2']) | |
}) | |
}) | |
}) | |
await t.test('value is/expects null', async t => { | |
await t.test('value has unexpected type', async t => { | |
await t.test('throws descriptive TypeError', t => { | |
t.assert.throws(() => validated({}, { ttl: 'null' }), { | |
name: 'TypeError', | |
message: /'ttl' must be/ | |
}) | |
}) | |
}) | |
await t.test('value has expected type', async t => { | |
await t.test('returns the value', t => { | |
t.assert.strictEqual(validated(null, { ttl: 'null' }), null) | |
}) | |
}) | |
}) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment