├── .npmrc ├── .gitignore ├── .npmignore ├── src ├── identity.ts ├── unknown.ts ├── or.test.ts ├── one-of.test.ts ├── never.ts ├── partial.test.ts ├── error-of.test.ts ├── simple.test.ts ├── implies.ts ├── defined.test.ts ├── nullish.ts ├── defined.ts ├── non-nullish.ts ├── null-or.ts ├── ok.ts ├── null.ts ├── true.ts ├── false.ts ├── null-or.test.ts ├── number.ts ├── string.ts ├── array.test.ts ├── boolean.ts ├── if.ts ├── nullish-or.ts ├── undefined-or.ts ├── undefined.ts ├── finite.ts ├── in.ts ├── safe-integer.ts ├── non-blank-string.ts ├── eq.ts ├── exact.test.ts ├── rethrow.ts ├── keyed.test.ts ├── eventually.test.ts ├── positive.ts ├── error-of.ts ├── lt.ts ├── regexp.ts ├── gt.ts ├── predicate.ts ├── lte.ts ├── or.ts ├── gte.ts ├── instance.ts ├── range.test.ts ├── and.ts ├── ignore.ts ├── optional.test.ts ├── array.ts ├── sequence.ts ├── date-string.ts ├── never.test.ts ├── keyed.ts ├── date-string.test.ts ├── unique.test.ts ├── strftime.test.ts ├── record.ts ├── record.test.ts ├── unique.ts ├── predicate.test.ts ├── tuple.ts ├── one-of.ts ├── exact.ts ├── tuple.test.ts ├── object.test.ts ├── eventually.ts ├── partial.ts ├── object.ts ├── instance.test.ts ├── and.test.ts ├── prelude.ts ├── index.ts └── strftime.ts ├── .vscode └── settings.json ├── jest.config.json ├── tsconfig.json ├── .github └── workflows │ ├── test.yml │ └── codeql-analysis.yml ├── jest.snapshot-resolver.cjs ├── Makefile ├── License.md ├── package.json ├── Readme.md └── Changelog.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | cjs 3 | mjs 4 | node_modules 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .vscode 3 | *.test.* 4 | jest.* 5 | test 6 | -------------------------------------------------------------------------------- /src/identity.ts: -------------------------------------------------------------------------------- 1 | const identity = 2 | (value: T): T => 3 | value 4 | 5 | export default identity 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/*.js": { 4 | "when": "$(basename).ts" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/unknown.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | 3 | const unknown: Assert = 4 | value => 5 | value 6 | 7 | export default unknown 8 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "testEnvironment": "node", 3 | "testMatch": [ 4 | "/cjs/(*.)test.js" 5 | ], 6 | "snapshotResolver": "./jest.snapshot-resolver.cjs" 7 | } 8 | -------------------------------------------------------------------------------- /src/or.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('or', () => { 4 | const assert = $.or($.number, $.string) 5 | const r = assert(JSON.parse('"foo"')) 6 | expect(r).toEqual('foo') 7 | }) 8 | -------------------------------------------------------------------------------- /src/one-of.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('oneOf', () => { 4 | const toggle = $.oneOf('ON', 'OFF') 5 | const value = toggle(JSON.parse('"ON"')) 6 | expect(value).toBe('ON') 7 | }) 8 | -------------------------------------------------------------------------------- /src/never.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | 3 | export const never_ = 4 | (value: never): never => { 5 | throw new TypeError(`Expected never, got ${inspect(value)}.`) 6 | } 7 | 8 | export default never_ 9 | -------------------------------------------------------------------------------- /src/partial.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('partial', () => { 4 | const r = $.partial({ foo: $.string, bar: $.number })(JSON.parse('{"foo":"foo"}')) 5 | expect(r).toEqual({ foo: 'foo' }) 6 | }) 7 | -------------------------------------------------------------------------------- /src/error-of.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('errorMessage', () => { 4 | expect($.errorOf($.string)('foo')).toEqual(undefined) 5 | expect($.errorOf($.string)(null)).toMatchObject({ message: 'Expected string, got null.' }) 6 | }) 7 | -------------------------------------------------------------------------------- /src/simple.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('array of strings', () => { 4 | expect(() => $.array($.string)(JSON.parse('["a", "b"]'))).not.toThrow() 5 | expect(() => $.array($.string)(JSON.parse('["a", 1, "b"]'))).toThrow() 6 | }) 7 | -------------------------------------------------------------------------------- /src/implies.ts: -------------------------------------------------------------------------------- 1 | import type { Assert, Predicate } from './prelude.js' 2 | 3 | const implies = 4 | (predicate: Predicate, a: Assert) => 5 | (value: T): T | A => 6 | predicate(value) ? 7 | a(value) : 8 | value 9 | 10 | export default implies 11 | -------------------------------------------------------------------------------- /src/defined.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('defined', () => { 4 | const a = $.object({ foo: $.defined }) 5 | const r = a(JSON.parse('{"foo":null}')) 6 | expect(r).toEqual({ foo: null }) 7 | expect(() => a(JSON.parse('{"bar":1}'))).toThrow('Expected defined.') 8 | }) 9 | -------------------------------------------------------------------------------- /src/nullish.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | 3 | const nullish: Assert = 4 | value => { 5 | if (value != null) { 6 | throw new TypeError('Expected nullish.') 7 | } 8 | return value as undefined | null 9 | } 10 | 11 | export default nullish 12 | -------------------------------------------------------------------------------- /src/defined.ts: -------------------------------------------------------------------------------- 1 | import type { Defined } from './prelude.js' 2 | 3 | const defined = 4 | (value: T): Defined => { 5 | if (typeof value === 'undefined') { 6 | throw new TypeError('Expected defined.') 7 | } 8 | return value as Defined 9 | } 10 | 11 | export default defined 12 | -------------------------------------------------------------------------------- /src/non-nullish.ts: -------------------------------------------------------------------------------- 1 | import type { NonNullish } from './prelude.js' 2 | 3 | const nonNullish = 4 | (value: T): NonNullish => { 5 | if (value == null) { 6 | throw new TypeError('Expected defined.') 7 | } 8 | return value as NonNullish 9 | } 10 | 11 | export default nonNullish 12 | -------------------------------------------------------------------------------- /src/null-or.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import rethrow from './rethrow.js' 3 | 4 | const nullOr: (a: Assert) => Assert = 5 | a => 6 | value => 7 | value === null ? 8 | null : 9 | rethrow(a, 'Was not null.')(value) 10 | 11 | export default nullOr 12 | -------------------------------------------------------------------------------- /src/ok.ts: -------------------------------------------------------------------------------- 1 | import type { Assert, Predicate } from './prelude.js' 2 | 3 | const ok: (a: Assert) => Predicate = 4 | a => 5 | value => { 6 | try { 7 | a(value) 8 | return true 9 | } catch (err) { 10 | return false 11 | } 12 | } 13 | 14 | export default ok 15 | -------------------------------------------------------------------------------- /src/null.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const null_: Assert = 5 | value => { 6 | if (value !== null) { 7 | throw new TypeError(`Expected null, got ${inspect(value)}.`) 8 | } 9 | return null 10 | } 11 | 12 | export default null_ 13 | -------------------------------------------------------------------------------- /src/true.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import type { Assert } from './prelude.js' 3 | 4 | const true_: Assert = 5 | value => { 6 | if (value !== true) { 7 | throw new TypeError(`Expected true, got ${inspect(value)}.`) 8 | } 9 | return value 10 | } 11 | 12 | export default true_ 13 | -------------------------------------------------------------------------------- /src/false.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import type { Assert } from './prelude.js' 3 | 4 | const false_: Assert = 5 | value => { 6 | if (value !== false) { 7 | throw new TypeError(`Expected false, got ${inspect(value)}.`) 8 | } 9 | return value 10 | } 11 | 12 | export default false_ 13 | -------------------------------------------------------------------------------- /src/null-or.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('simple', () => { 4 | const union = $.or($.string, $.number) 5 | const assert = $.nullOr(union) 6 | expect(assert(null)).toEqual(null) 7 | expect(assert(1)).toEqual(1) 8 | expect(assert('foo')).toEqual('foo') 9 | expect(() => assert(true)).toThrow('') 10 | }) 11 | -------------------------------------------------------------------------------- /src/number.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import type { Assert } from './prelude.js' 3 | 4 | const number: Assert = 5 | value => { 6 | if (typeof value !== 'number') { 7 | throw new TypeError(`Expected number, got ${inspect(value)}.`) 8 | } 9 | return value 10 | } 11 | 12 | export default number 13 | -------------------------------------------------------------------------------- /src/string.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import type { Assert } from './prelude.js' 3 | 4 | const string_: Assert = 5 | value => { 6 | if (typeof value !== 'string') { 7 | throw new TypeError(`Expected string, got ${inspect(value)}.`) 8 | } 9 | return value 10 | } 11 | 12 | export default string_ 13 | -------------------------------------------------------------------------------- /src/array.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('array', () => { 4 | const r: string[] = $.array($.string)(JSON.parse('["a","b"]')) 5 | expect(r).toEqual([ 'a', 'b' ]) 6 | }) 7 | 8 | test('array error', () => { 9 | expect(() => $.array($.string)(JSON.parse('["a",42]'))).toThrow('[1] Expected string, got 42.') 10 | }) 11 | -------------------------------------------------------------------------------- /src/boolean.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const boolean: Assert = 5 | value => { 6 | if (typeof value !== 'boolean') { 7 | throw new TypeError(`Expected boolean, got ${inspect(value)}.`) 8 | } 9 | return value 10 | } 11 | 12 | export default boolean 13 | -------------------------------------------------------------------------------- /src/if.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | 3 | type Predicate = (value: T) => boolean 4 | 5 | const if_ = 6 | (predicate: Predicate, then_: Assert, else_: Assert) => 7 | (value: T): A | B => 8 | predicate(value) ? 9 | then_(value) : 10 | else_(value) 11 | 12 | export default if_ 13 | -------------------------------------------------------------------------------- /src/nullish-or.ts: -------------------------------------------------------------------------------- 1 | import rethrow from './rethrow.js' 2 | import type { Assert, Nullish } from './prelude.js' 3 | 4 | const nullishOr = 5 | (a: Assert): Assert> => 6 | value => 7 | value == null ? 8 | value as undefined | null : 9 | rethrow(a, 'Was not nullish.')(value) 10 | 11 | export default nullishOr 12 | -------------------------------------------------------------------------------- /src/undefined-or.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import rethrow from './rethrow.js' 3 | 4 | const undefinedOr: (a: Assert) => Assert = 5 | a => 6 | value => 7 | typeof value === 'undefined' ? 8 | undefined : 9 | rethrow(a, 'Was not undefined.')(value) 10 | 11 | export default undefinedOr 12 | -------------------------------------------------------------------------------- /src/undefined.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const undefined_: Assert = 5 | value => { 6 | if (typeof value !== 'undefined') { 7 | throw new TypeError(`Expected undefined, got ${inspect(value)}.`) 8 | } 9 | return undefined 10 | } 11 | 12 | export default undefined_ 13 | -------------------------------------------------------------------------------- /src/finite.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const finite: Assert = 5 | value => { 6 | if (typeof value !== 'number' || !Number.isFinite(value)) { 7 | throw new TypeError(`Expected finite number, got ${inspect(value)}.`) 8 | } 9 | return value 10 | } 11 | 12 | export default finite 13 | -------------------------------------------------------------------------------- /src/in.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const in_ = 5 | (values: T[]): Assert => 6 | value => { 7 | if (!values.includes(value as T)) { 8 | throw new TypeError(`Expected ${inspect(value)} to be in ${inspect(values)}.`) 9 | } 10 | return value as T 11 | } 12 | 13 | export default in_ 14 | -------------------------------------------------------------------------------- /src/safe-integer.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const safeInteger: Assert = 5 | value => { 6 | if (typeof value !== 'number' || !Number.isSafeInteger(value)) { 7 | throw new TypeError(`Expected safe integer, got ${inspect(value)}.`) 8 | } 9 | return value 10 | } 11 | 12 | export default safeInteger 13 | -------------------------------------------------------------------------------- /src/non-blank-string.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import type { Assert } from './prelude.js' 3 | 4 | const nonBlankString: Assert = 5 | value => { 6 | if (typeof value !== 'string' || value.trim() === '') { 7 | throw new TypeError(`Expected non-blank string, got ${inspect(value)}`) 8 | } 9 | return value 10 | } 11 | 12 | export default nonBlankString 13 | -------------------------------------------------------------------------------- /src/eq.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import type { Assert, Primitive } from './prelude.js' 3 | 4 | const eq = 5 | (expected: T): Assert => 6 | value => { 7 | if (value !== expected) { 8 | throw new TypeError(`Expected ${inspect(expected)}, got ${inspect(value)}.`) 9 | } 10 | return value as T 11 | } 12 | 13 | export default eq 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ES2020", 5 | "moduleResolution": "Node", 6 | "noImplicitAny": true, 7 | "noImplicitReturns": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true 10 | }, 11 | "include": [ 12 | "src" 13 | ], 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/exact.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('exact ok', () => { 4 | const value = $.exact({ foo: $.string, bar: $.number })(JSON.parse('{"foo":"a","bar":1}')) 5 | expect(value).toEqual({ foo: 'a', bar: 1 }) 6 | }) 7 | 8 | test('exact', () => { 9 | expect(() => $.exact({ foo: $.string })(JSON.parse('{"foo":"a","bar":1,"baz":2}'))).toThrow('Unexpected extra keys bar, baz in { foo: \'a\', bar: 1, baz: 2 }.') 10 | }) 11 | -------------------------------------------------------------------------------- /src/rethrow.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | 3 | const rethrow: (a: Assert, message: string) => Assert = 4 | (a, message) => 5 | value => { 6 | try { 7 | return a(value) 8 | } catch (err: unknown) { 9 | if (err instanceof Error) { 10 | err.message = `${message} ${err.message}` 11 | } 12 | throw err 13 | } 14 | } 15 | 16 | export default rethrow 17 | -------------------------------------------------------------------------------- /src/keyed.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('keyed', () => { 4 | expect($.keyed($.boolean)(JSON.parse('{"foo":true,"bar":false}'))).toEqual({ 5 | foo: true, 6 | bar: false 7 | }) 8 | expect(() => $.keyed($.boolean)(JSON.parse('{"foo":true,"bar":null}'))) 9 | .toThrow('[\'bar\'] Expected boolean, got null.') 10 | expect(() => $.keyed($.boolean)([ true ])).toThrow('Expected object, got [ true ].') 11 | }) 12 | -------------------------------------------------------------------------------- /src/eventually.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('eventually', async () => { 4 | const before = Date.now() 5 | await expect($.eventually($.gt(before + 1000), async () => Date.now())) 6 | .resolves.not.toThrow() 7 | }) 8 | 9 | test('eventually fails', async () => { 10 | await expect($.eventually($.gt(2), async () => 1, { delay: 10 })) 11 | .rejects.toThrow('Expected 1 to be greater than 2. Last result was 1.') 12 | }) 13 | -------------------------------------------------------------------------------- /src/positive.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import type { Assert } from './prelude.js' 3 | 4 | const positive: Assert = 5 | value => { 6 | if (typeof value !== 'number') { 7 | throw new TypeError(`Expected number, got ${inspect(value)}.`) 8 | } 9 | if (value <= 0) { 10 | throw new TypeError(`Expected positive number, got ${inspect(value)}.`) 11 | } 12 | return value 13 | } 14 | 15 | export default positive 16 | -------------------------------------------------------------------------------- /src/error-of.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | 3 | /** @returns `Error` of unsatisfied assertion, `undefined` if assertion passes. */ 4 | const errorOf = 5 | (a: Assert): Assert => 6 | value => { 7 | try { 8 | a(value) 9 | return 10 | } catch (err) { 11 | return err instanceof Error ? 12 | err : 13 | new Error(String(err)) 14 | } 15 | } 16 | 17 | export default errorOf 18 | -------------------------------------------------------------------------------- /src/lt.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const lt: (than: number) => Assert = 5 | than => 6 | value => { 7 | if (typeof value !== 'number') { 8 | throw new TypeError(`Expected number, got ${inspect(value)}.`) 9 | } 10 | if (!(value < than)) { 11 | throw new TypeError(`Expected ${inspect(value)} to be less than ${inspect(than)}.`) 12 | } 13 | return value 14 | } 15 | 16 | export default lt 17 | -------------------------------------------------------------------------------- /src/regexp.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const regexp: (re: RegExp) => Assert = 5 | re => 6 | value => { 7 | if (typeof value !== 'string') { 8 | throw new TypeError(`Expected string, got ${inspect(value)}.`) 9 | } 10 | if (!value.match(re)) { 11 | throw new TypeError(`Expected ${inspect(value)} to match ${inspect(re)}.`) 12 | } 13 | return value 14 | } 15 | 16 | export default regexp 17 | -------------------------------------------------------------------------------- /src/gt.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const gt: (than: number) => Assert = 5 | than => 6 | value => { 7 | if (typeof value !== 'number') { 8 | throw new TypeError(`Expected number, got ${inspect(value)}.`) 9 | } 10 | if (!(value > than)) { 11 | throw new TypeError(`Expected ${inspect(value)} to be greater than ${inspect(than)}.`) 12 | } 13 | return value 14 | } 15 | 16 | export default gt 17 | -------------------------------------------------------------------------------- /src/predicate.ts: -------------------------------------------------------------------------------- 1 | import type { Predicate } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | type Identity = (value: T) => T 5 | 6 | const predicate: (f: Predicate, name?: string) => Identity = 7 | (f, name) => 8 | value => { 9 | if (!f(value)) { 10 | throw new TypeError(`Expected ${name === undefined ? 'predicate' : `${inspect(name)} predicate`} to pass for ${inspect(value)}.`) 11 | } 12 | return value 13 | } 14 | 15 | export default predicate 16 | -------------------------------------------------------------------------------- /src/lte.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const lte: (than: number) => Assert = 5 | than => 6 | value => { 7 | if (typeof value !== 'number') { 8 | throw new TypeError(`Expected number, got ${inspect(value)}.`) 9 | } 10 | if (!(value <= than)) { 11 | throw new TypeError(`Expected ${inspect(value)} to be less than or equal to ${inspect(than)}.`) 12 | } 13 | return value 14 | } 15 | 16 | export default lte 17 | -------------------------------------------------------------------------------- /src/or.ts: -------------------------------------------------------------------------------- 1 | import type { Assert, Asserted } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const or = 5 | []>(...as: As) => 6 | (value: unknown) => { 7 | for (const a of as) { 8 | try { 9 | return a(value) as Asserted 10 | } catch (err) { 11 | continue 12 | } 13 | } 14 | throw new TypeError(`Expected one of ${as.length} alternatives to match ${inspect(value)}.`) 15 | } 16 | 17 | export default or 18 | -------------------------------------------------------------------------------- /src/gte.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const gte: (than: number) => Assert = 5 | than => 6 | value => { 7 | if (typeof value !== 'number') { 8 | throw new TypeError(`Expected number, got ${inspect(value)}.`) 9 | } 10 | if (!(value >= than)) { 11 | throw new TypeError(`Expected ${inspect(value)} to be greater than or equal to ${inspect(than)}.`) 12 | } 13 | return value 14 | } 15 | 16 | export default gte 17 | -------------------------------------------------------------------------------- /src/instance.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const instance = 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | any>(class_: T): Assert> => 7 | value => { 8 | if (!(value instanceof class_)) { 9 | throw new TypeError(`Expected ${inspect(value)} to be instance of ${inspect(class_)}.`) 10 | } 11 | return value as InstanceType 12 | } 13 | 14 | export default instance 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [16.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm run build --if-present 22 | - run: npm test 23 | env: 24 | CI: true 25 | -------------------------------------------------------------------------------- /jest.snapshot-resolver.cjs: -------------------------------------------------------------------------------- 1 | const resolveSnapshotPath = 2 | (testPath, snapshotExtension) => 3 | testPath 4 | .replace('.test' + '.js', '.test' + snapshotExtension) 5 | .replace('/cjs/', '/src/') 6 | 7 | const resolveTestPath = 8 | (snapshotFilePath, snapshotExtension) => 9 | snapshotFilePath 10 | .replace(snapshotExtension, '.js') 11 | .replace('/src/', '/cjs/') 12 | 13 | const testPathForConsistencyCheck = 14 | './cjs/index.test.js' 15 | 16 | module.exports = { 17 | resolveSnapshotPath, 18 | resolveTestPath, 19 | testPathForConsistencyCheck 20 | } 21 | -------------------------------------------------------------------------------- /src/range.test.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import * as $ from './index.js' 3 | 4 | export type Range = [ number, number ] 5 | 6 | const range: $.Assert = 7 | $.sequence( 8 | $.tuple($.number, $.number), 9 | _ => { 10 | if (_[0] > _[1]) { 11 | throw new TypeError(`Expected [a, b] range where a <= b, got ${inspect(_)}.`) 12 | } 13 | return _ 14 | } 15 | ) 16 | 17 | test('assert, range', () => { 18 | expect(range([ 1, 2 ])).toEqual([ 1, 2 ]) 19 | expect(() => range([ 2, 1 ])).toThrow('Expected [a, b] range where a <= b, got [ 2, 1 ].') 20 | }) 21 | -------------------------------------------------------------------------------- /src/and.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | 3 | type Z = 4 | (value: A) => 5 | B 6 | 7 | type And = { 8 | (a: Assert): Assert 9 | (a: Assert, b: Z): Assert 10 | (a: Assert, b: Z, c: Z): Assert 11 | (a: Assert, b: Z, c: Z, d: Z): Assert 12 | } 13 | 14 | const and: And = 15 | (...as: Assert[]) => 16 | (value: unknown) => { 17 | for (const a of as) { 18 | a(value) 19 | } 20 | return value 21 | } 22 | 23 | export default and 24 | -------------------------------------------------------------------------------- /src/ignore.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | 3 | /** Ignores error returning value as is. Optional on-error handler can be provided to ie. log warning. */ 4 | const ignore = 5 | (a: Assert, onError?: (err: Error) => void): Assert => 6 | value => { 7 | try { 8 | return a(value) 9 | } catch (err) { 10 | if (onError) { 11 | onError( 12 | err instanceof Error ? 13 | err : 14 | new Error(String(err)) 15 | ) 16 | } 17 | return value as T 18 | } 19 | } 20 | 21 | export default ignore 22 | -------------------------------------------------------------------------------- /src/optional.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('nullishOr', () => { 4 | expect(() => $.object({ foo: $.nullishOr($.number) })({ foo: 'bar' })) 5 | .toThrow('[foo] Was not nullish. Expected number, got \'bar\'.') 6 | }) 7 | 8 | test('nullOr', () => { 9 | expect(() => $.object({ foo: $.nullOr($.number) })({ foo: 'bar' })) 10 | .toThrow('[foo] Was not null. Expected number, got \'bar\'.') 11 | }) 12 | 13 | test('undefinedOr', () => { 14 | expect(() => $.object({ foo: $.undefinedOr($.number) })({ foo: 'bar' })) 15 | .toThrow('[foo] Was not undefined. Expected number, got \'bar\'.') 16 | }) 17 | -------------------------------------------------------------------------------- /src/array.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import unknown from './unknown.js' 3 | 4 | const array: (a: Assert) => Assert = 5 | a => 6 | value => { 7 | if (!Array.isArray(value)) { 8 | throw new TypeError('Expected array.') 9 | } 10 | if (a && a !== unknown) { 11 | value.forEach((_, i) => { 12 | try { 13 | a(_) 14 | } catch (err: unknown) { 15 | throw new TypeError(`[${i}] ${err instanceof Error ? err.message : err}`) 16 | } 17 | }) 18 | } 19 | return value 20 | } 21 | 22 | export default array 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | @rm -Rf cjs mjs test/*.js 3 | 4 | build-cjs: 5 | @rm -Rf cjs 6 | @npx tsc -m commonjs -d --sourceMap --outDir cjs 7 | @echo '{"type":"commonjs"}' > cjs/package.json 8 | 9 | build-mjs: 10 | @rm -Rf mjs 11 | @npx tsc -d --sourceMap --outDir mjs 12 | 13 | build: build-cjs build-mjs 14 | 15 | rebuild: clean build 16 | 17 | test: rebuild 18 | @npx jest 19 | 20 | update: 21 | @npx npm-check --update --save-exact 22 | 23 | postversion: 24 | @npx changelog > Changelog.md 25 | @git add Changelog.md 26 | @git commit -m "Updating changelog." 27 | @git push 28 | @git push --tags 29 | @npm publish 30 | 31 | .PHONY: test 32 | -------------------------------------------------------------------------------- /src/sequence.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | 3 | type $ = (a: A) => B 4 | 5 | type Sequence = { 6 | (a: Assert): Assert 7 | (a: Assert, b: $): Assert 8 | (a: Assert, b: $, c: $): Assert 9 | } 10 | 11 | const sequence: Sequence = 12 | (...as: Assert[]) => { 13 | if (!as.length) { 14 | throw new TypeError('Expected non-zero arity.') 15 | } 16 | return (value: unknown) => { 17 | let r = value 18 | for (const a of as) { 19 | r = a(r) 20 | } 21 | return r 22 | } 23 | } 24 | 25 | export default sequence 26 | -------------------------------------------------------------------------------- /src/date-string.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const re = /^\d{4}-\d{2}-\d{2}$/ 5 | 6 | const dateString: Assert = 7 | value => { 8 | if (typeof value !== 'string') { 9 | throw new TypeError(`Expected string, got ${inspect(value)}.`) 10 | } 11 | if (!value.match(re)) { 12 | throw new TypeError(`Expected YYYY-MM-DD date string, got ${inspect(value)}.`) 13 | } 14 | if (isNaN(new Date(value).getTime())) { 15 | throw new TypeError(`Expected valid date string, got ${inspect(value)}.`) 16 | } 17 | return value 18 | } 19 | 20 | export default dateString 21 | -------------------------------------------------------------------------------- /src/never.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | type X = 'a' | 'b' | 'c' 4 | 5 | const assertX: $.Assert = 6 | $.oneOf('a', 'b', 'c') 7 | 8 | test('never', () => { 9 | const x: X = assertX(JSON.parse('"c"')) 10 | expect(() => { 11 | switch (x) { 12 | case 'a': 13 | case 'b': 14 | expect(typeof x).toBe('string') 15 | break 16 | default: 17 | 18 | // @ts-expect-error(exhaustive) 19 | $.never(x) 20 | } 21 | }).toThrow('Expected never, got \'c\'.') 22 | switch (x) { 23 | case 'a': 24 | case 'b': 25 | case 'c': 26 | expect(typeof x).toBe('string') 27 | break 28 | default: 29 | $.never(x) 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /src/keyed.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import rethrow from './rethrow.js' 3 | import type { Assert, Keyed } from './prelude.js' 4 | 5 | /** @returns assert for `Keyed`, which is check-forced record `Record`. */ 6 | const keyed = 7 | (a: Assert): Assert> => 8 | value => { 9 | if (value === null || typeof value !== 'object' || Array.isArray(value)) { 10 | throw new TypeError(`Expected object, got ${inspect(value)}.`) 11 | } 12 | const value_ = value as Record 13 | for (const key in value_) { 14 | rethrow(a, `[${inspect(key)}]`)(value_[key]) 15 | } 16 | return value_ as Keyed 17 | } 18 | 19 | export default keyed 20 | -------------------------------------------------------------------------------- /src/date-string.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | const dateRange = $.exact({ 4 | from: $.dateString, 5 | to: $.dateString 6 | }) 7 | 8 | test('valid', () => { 9 | expect(dateRange(JSON.parse('{"from":"2001-01-01","to":"2001-01-02"}'))).toEqual({ 10 | from: '2001-01-01', 11 | to: '2001-01-02' 12 | }) 13 | }) 14 | 15 | test('not valid date string', () => { 16 | expect(() => dateRange(JSON.parse('{"from":"2001-01-01","to":"today"}'))).toThrow( 17 | 'Expected YYYY-MM-DD date string, got \'today\'.' 18 | ) 19 | }) 20 | 21 | test('not a valid date', () => { 22 | expect(() => dateRange(JSON.parse('{"from":"2001-01-01","to":"2001-13-01"}'))).toThrow( 23 | 'Expected valid date string, got \'2001-13-01\'.' 24 | ) 25 | }) 26 | -------------------------------------------------------------------------------- /src/unique.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('unique', () => { 4 | expect(() => $.unique(_ => String(_))([ 'b', 'a', 'c', 'a' ])).toThrow('Expected unique values, got duplicate \'a\'.') 5 | expect($.unique(_ => String(_))([ 'b', 'c', 'a' ])).toEqual([ 'b', 'c', 'a' ]) 6 | }) 7 | 8 | test('unique objects', () => { 9 | const f = (_: { name: string }) => _.name.trim().toLowerCase() 10 | expect(() => $.unique(f)([ 11 | { name: 'a' }, 12 | { name: 'C' }, 13 | { name: 'A ' } 14 | ])).toThrow('Expected unique values, got duplicate { name: \'A \' }.') 15 | expect($.unique(f)([ 16 | { name: 'a' }, 17 | { name: 'c' }, 18 | { name: 'b' } 19 | ])).toEqual([ 20 | { name: 'a' }, 21 | { name: 'c' }, 22 | { name: 'b' } 23 | ]) 24 | }) 25 | -------------------------------------------------------------------------------- /src/strftime.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('simple', () => { 4 | expect($.strftime('%Y-%m-%d')('2000-01-01')).toEqual('2000-01-01') 5 | expect(() => $.strftime('%Y-%m-%d')('2000-01-01 ')).toThrow( 6 | 'Expected \'%Y-%m-%d\' strftime format, got \'2000-01-01 \', failed at index 10, unrecognised part \' \'.' 7 | ) 8 | expect(() => $.strftime('%Y-%m-%d')('2000-01-32')).toThrow( 9 | 'Expected \'%Y-%m-%d\' strftime format, got \'2000-01-32\', failed at index 8, unrecognised part \'32\'.' 10 | ) 11 | expect(() => $.strftime('%Y-%m-%d')('2000-13-01')).toThrow( 12 | 'Expected \'%Y-%m-%d\' strftime format, got \'2000-13-01\', failed at index 5, unrecognised part \'13-01\'.' 13 | ) 14 | const d = new Date().toISOString() 15 | expect($.strftime('%FT%T.%sZ')(d)).toEqual(d) 16 | }) 17 | -------------------------------------------------------------------------------- /src/record.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | import rethrow from './rethrow.js' 4 | 5 | // TODO: Allow number indexer; maybe create dedicated combinator for numeric indexers? 6 | const record = 7 | (k: Assert, v: Assert): Assert<{ [key: string]: V }> => 8 | value => { 9 | if (value === null || typeof value !== 'object') { 10 | throw new TypeError(`Expected object, got ${inspect(value)}.`) 11 | } 12 | const value_ = value as { [key: string]: unknown } 13 | const k_ = rethrow(k, 'Indexer key.') 14 | for (const key in value_) { 15 | k_(key) 16 | rethrow(v, `[${inspect(key)}]`)(value_[key]) 17 | } 18 | return value_ as { [key: string]: V } 19 | } 20 | 21 | export default record 22 | -------------------------------------------------------------------------------- /src/record.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | type My = { 4 | defaultA?: $.Nullish, 5 | defaultB?: $.Nullish 6 | } & { [key: string]: number } 7 | 8 | const my: $.Assert = 9 | (value: unknown) => { 10 | const { defaultA, defaultB, ...rest } = $.object({ 11 | defaultA: $.nullishOr($.number), 12 | defaultB: $.nullishOr($.number) 13 | })(value) 14 | return { 15 | ...$.record($.string, $.number)(rest), 16 | defaultA, 17 | defaultB 18 | } as My 19 | } 20 | 21 | test('indexer', () => { 22 | const value = my({ defaultA: null, defaultB: 1, a: 1 }) 23 | expect(value).toEqual({ 24 | defaultA: null, 25 | defaultB: 1, 26 | a: 1 27 | }) 28 | expect(my({ a: 2 })).toEqual({ a: 2 }) 29 | expect(() => my({ a: '1' })).toThrow('[\'a\'] Expected number, got \'1\'.') 30 | }) 31 | -------------------------------------------------------------------------------- /src/unique.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import type { Assert, Primitive } from './prelude.js' 3 | 4 | type Mapping = (value: A) => B 5 | 6 | /** Asserts unique array using given `f` mapping from array elements to `===` comparable values (defaults to identity). */ 7 | const unique = 8 | (f: Mapping): Assert => 9 | values => { 10 | if (!Array.isArray(values)) { 11 | throw new TypeError(`Expected array, got ${inspect(values)}.`) 12 | } 13 | const set = new Set 14 | for (const value of values) { 15 | const value_ = f(value) 16 | if (set.has(value_)) { 17 | throw new TypeError(`Expected unique values, got duplicate ${inspect(value)}.`) 18 | } 19 | set.add(value_) 20 | } 21 | return values 22 | } 23 | 24 | export default unique 25 | -------------------------------------------------------------------------------- /src/predicate.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | const user = 4 | $.object({ name: $.string, role: $.string }) 5 | 6 | const roleExists = 7 | (root: any) => 8 | $.predicate((_: any) => root.roles.includes(_.role), 'role exists') 9 | 10 | const message = 11 | (root: any) => 12 | $.object({ 13 | roles: $.array($.string), 14 | users: $.array($.and(user, roleExists(root))) 15 | })(root) 16 | 17 | test('valid', () => { 18 | expect(() => message({ 19 | roles: [ 'admin' ], 20 | users: [ { name: 'Anna', role: 'admin' } ] 21 | })).not.toThrow() 22 | }) 23 | 24 | test('invalid, role doesn\'t exist', () => { 25 | expect(() => message({ 26 | roles: [ 'admin' ], 27 | users: [ { name: 'Anna', role: 'ADMIN' } ] 28 | })).toThrow('[users] [0] Expected \'role exists\' predicate to pass for { name: \'Anna\', role: \'ADMIN\' }.') 29 | }) 30 | -------------------------------------------------------------------------------- /src/tuple.ts: -------------------------------------------------------------------------------- 1 | import type { Assert, Asserted } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | const tuple = 5 | []]>(...as: T): Assert<{ [I in keyof T]: Asserted }> => 6 | (value: unknown) => { 7 | if (!Array.isArray(value)) { 8 | throw new TypeError(`Expected tuple, got ${inspect(value)}.`) 9 | } 10 | if (value.length > as.length) { 11 | throw new TypeError(`Expected tuple length of not more than ${as.length}, got ${value.length} in ${inspect(value)}.`) 12 | } 13 | as.forEach((a, i) => { 14 | try { 15 | a(value[i]) 16 | } catch (err: unknown) { 17 | throw new TypeError(`[${i}] ${err instanceof Error ? err.message : err}`) 18 | } 19 | }) 20 | return value as { [I in keyof T]: Asserted } 21 | } 22 | 23 | export default tuple 24 | -------------------------------------------------------------------------------- /src/one-of.ts: -------------------------------------------------------------------------------- 1 | import type { Assert } from './prelude.js' 2 | import { inspect } from 'util' 3 | 4 | /** 5 | * Asserts primitive type union. 6 | * 7 | * @example 8 | * import * as $ from '@appliedblockchain/assert-combinators' 9 | * const assert = $.oneOf('A', 'B', 'C') 10 | * export type T = $.Asserted 11 | * // type T is 'A' | 'B' | 'C' 12 | * 13 | */ 14 | const oneOf = 15 | (...values: readonly T[]): Assert => { 16 | if (!values.length) { 17 | throw new TypeError('Expected non-zero arity.') 18 | } 19 | return (value: unknown) => { 20 | for (const value_ of values) { 21 | if (value_ === value) { 22 | return value as T 23 | } 24 | } 25 | throw new TypeError(`Expected ${inspect(value)} to be one of ${inspect(values)}.`) 26 | } 27 | } 28 | 29 | export default oneOf 30 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ master ] 9 | schedule: 10 | - cron: '40 4 * * 0' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | language: [ 'javascript', 'typescript' ] 25 | 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v2 29 | 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v1 32 | with: 33 | languages: ${{ matrix.language }} 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v1 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v1 40 | -------------------------------------------------------------------------------- /src/exact.ts: -------------------------------------------------------------------------------- 1 | import type { Assert, Asserted, Primitive, OptionalIfUndefined } from './prelude.js' 2 | import { inspect } from 'util' 3 | import object from './object.js' 4 | 5 | const exact = 6 | >>(kvs: T): Assert }>> => 7 | value => { 8 | if (typeof value !== 'object' || value === null) { 9 | throw new TypeError(`Expected object, got ${inspect(value)}.`) 10 | } 11 | const result = object(kvs)(value) 12 | const keys = Object 13 | .keys(value) 14 | .filter(key => !Object.prototype.hasOwnProperty.call(kvs, key)) 15 | if (keys.length) { 16 | throw new TypeError(`Unexpected extra keys ${keys.map(String).join(', ')} in ${inspect(value)}.`) 17 | } 18 | return result as OptionalIfUndefined<{ [k in keyof T]: T[k] extends Primitive ? T[k] : Asserted }> 19 | } 20 | 21 | export default exact 22 | -------------------------------------------------------------------------------- /src/tuple.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('tuple', () => { 4 | const r = $.tuple($.number, $.string)(JSON.parse('[1,"foo"]')) 5 | expect(r).toEqual([ 1, 'foo' ]) 6 | }) 7 | 8 | test('more', () => { 9 | expect(() => $.tuple($.number, $.string)(JSON.parse('[1,"foo","baz"]'))).toThrow('Expected tuple length of not more than 2, got 3 in [ 1, \'foo\', \'baz\' ].') 10 | }) 11 | 12 | test('less', () => { 13 | expect(() => $.tuple($.number, $.string)(JSON.parse('[1]'))).toThrow('[1] Expected string, got undefined.') 14 | }) 15 | 16 | test('optional tail', () => { 17 | expect($.tuple($.number, $.undefinedOr($.string))(JSON.parse('[1]'))).toEqual([ 1 ]) 18 | expect($.tuple($.number, $.undefinedOr($.string))(JSON.parse('[1,"foo"]'))).toEqual([ 1, 'foo' ]) 19 | }) 20 | 21 | test('undefined, null, nullish', () => { 22 | const r = $.tuple( 23 | $.undefinedOr($.number), 24 | $.nullOr($.string), 25 | $.nullishOr($.boolean) 26 | )(JSON.parse('[1,"foo"]')) 27 | expect(r).toEqual([ 1, 'foo' ]) 28 | }) 29 | -------------------------------------------------------------------------------- /src/object.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | export type My = { 4 | id: number, 5 | uuid: string, 6 | reason: null | string 7 | } 8 | 9 | export const assert: $.Assert = 10 | $.object({ 11 | id: $.number, 12 | uuid: $.string, 13 | reason: $.nullOr($.string) 14 | }) 15 | 16 | export type My2 = $.Asserted 17 | 18 | test('object', () => { 19 | const r = $.object({ foo: $.string, bar: $.number })(JSON.parse('{"foo":"a","bar":1}')) 20 | expect(r).toEqual({ foo: 'a', bar: 1 }) 21 | }) 22 | 23 | test('error', () => { 24 | expect(() => $.object({ foo: $.string, bar: $.number })(JSON.parse('{"foo":"a","bar":"2"}'))).toThrow('[bar] Expected number, got \'2\'.') 25 | }) 26 | 27 | test('primitive', () => { 28 | const assert = $.object({ 29 | foo: 'FOO' as const, 30 | bar: 1 as const, 31 | baz: true as const 32 | }) 33 | expect(() => assert({ foo: 'FOO', bar: 1, baz: true, xyz: 'yes' })).not.toThrow() 34 | expect(() => assert({ foo: 1 })).toThrow('Expected \'foo\' to be \'FOO\', got 1 in { foo: 1 } object.') 35 | }) 36 | -------------------------------------------------------------------------------- /License.md: -------------------------------------------------------------------------------- 1 | ## License 2 | 3 | MIT License 4 | 5 | Copyright 2019 Applied Blockchain 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /src/eventually.ts: -------------------------------------------------------------------------------- 1 | const defaultError = 2 | (err: unknown, result: T) => 3 | new Error((err instanceof Error ? err.message : String(err)) + ' Last result was ' + JSON.stringify(result) + '.') 4 | 5 | const eventually = 6 | async (assert: (result: T) => U, poll: () => Promise, { 7 | retries = 10, 8 | delay = 1000, 9 | error = defaultError 10 | }: { 11 | retries?: number, 12 | delay?: number, 13 | 14 | /** Error constructor based on thrown assertion error and last polled result. */ 15 | error?: (err: unknown, result: T) => unknown 16 | } = {}): Promise => { 17 | for (let retry = 1; retry <= retries; retry++) { 18 | const before = Date.now() 19 | const result = await poll() 20 | try { 21 | return assert(result) 22 | } catch (err) { 23 | if (retry === retries) { 24 | throw error(err, result) 25 | } 26 | } 27 | const duration = Date.now() - before 28 | await new Promise(resolve => setTimeout(resolve, delay - duration)) 29 | } 30 | throw new Error('Unreachable.') 31 | } 32 | 33 | export default eventually 34 | -------------------------------------------------------------------------------- /src/partial.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import type { Assert, Asserted, Primitive } from './prelude.js' 3 | 4 | const partial = 5 | >>(kvs: T): Assert<{ [k in keyof T]?: T[k] extends Primitive ? T[k] : Asserted }> => 6 | value => { 7 | if (typeof value !== 'object' || value == null) { 8 | throw new TypeError('Expected object.') 9 | } 10 | for (const k in kvs) { 11 | if (typeof (value as Record)[k] === 'undefined') { 12 | continue 13 | } 14 | const v = kvs[k] 15 | if (typeof v === 'function') { 16 | try { 17 | v((value as Record)[k]) 18 | } catch (err: unknown) { 19 | throw new TypeError(`[${k}] ${err instanceof Error ? err.message : err}`) 20 | } 21 | } else if (v !== (value as Record)[k]) { 22 | throw new TypeError(`Expected ${inspect(k)} to be ${inspect(v)}, got ${inspect((value as Record)[k])} in ${inspect(value)} object.`) 23 | } 24 | } 25 | return value 26 | } 27 | 28 | export default partial 29 | -------------------------------------------------------------------------------- /src/object.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import type { Assert, Asserted, Primitive, OptionalIfUndefined } from './prelude.js' 3 | 4 | const object = 5 | >>(kvs: T): Assert }>> => 6 | value => { 7 | if (typeof value !== 'object' || value === null) { 8 | throw new TypeError(`Expected object, got ${inspect(value)}.`) 9 | } 10 | for (const k in kvs) { 11 | const v = kvs[k] 12 | if (typeof v === 'function') { 13 | try { 14 | v((value as Record)[k]) 15 | } catch (err: unknown) { 16 | throw new TypeError(`[${k}] ${err instanceof Error ? err.message : err}`) 17 | } 18 | } else if (v !== (value as Record)[k]) { 19 | throw new TypeError(`Expected ${inspect(k)} to be ${inspect(v)}, got ${inspect((value as Record)[k])} in ${inspect(value)} object.`) 20 | } 21 | } 22 | return value as OptionalIfUndefined<{ [k in keyof T]: T[k] extends Primitive ? T[k] : Asserted }> 23 | } 24 | 25 | export default object 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@appliedblockchain/assert-combinators", 3 | "version": "5.7.0", 4 | "description": "Assertion combinators.", 5 | "type": "module", 6 | "main": "./cjs/index.js", 7 | "module": "./mjs/index.js", 8 | "exports": { 9 | ".": { 10 | "import": "./mjs/index.js", 11 | "require": "./cjs/index.js" 12 | }, 13 | "./*.js": { 14 | "import": "./mjs/*.js", 15 | "require": "./cjs/*.js" 16 | }, 17 | "./*": { 18 | "require": "./cjs/*.js" 19 | } 20 | }, 21 | "scripts": { 22 | "test": "make test", 23 | "preversion": "make test", 24 | "postversion": "make postversion" 25 | }, 26 | "keywords": [], 27 | "author": "Mirek Rusin (https://github.com/mirek)", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "@appliedblockchain/changelog": "1.2.1", 31 | "@appliedblockchain/eslint-config": "3.1.1", 32 | "@tsconfig/node16": "16.1.1", 33 | "@types/jest": "29.5.11", 34 | "@typescript-eslint/eslint-plugin": "6.19.1", 35 | "@typescript-eslint/parser": "6.19.1", 36 | "eslint": "8.56.0", 37 | "eslint-plugin-jest": "27.6.3", 38 | "jest": "29.7.0", 39 | "npm-check": "6.0.1", 40 | "typescript": "5.3.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/instance.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | import { inspect } from 'util' 3 | 4 | class Shape { 5 | get area() { 6 | return 0 7 | } 8 | } 9 | 10 | class Circle extends Shape { 11 | r: number 12 | constructor(r: number) { 13 | super() 14 | this.r = r 15 | } 16 | get area() { 17 | return Math.PI * (this.r ** 2) 18 | } 19 | } 20 | 21 | class Square extends Shape { 22 | w: number 23 | constructor(w: number) { 24 | super() 25 | this.w = w 26 | } 27 | get area() { 28 | return this.w ** this.w 29 | } 30 | } 31 | 32 | const shapeOf = 33 | (value: { type: 'circle', r: number } | { type: 'square', w: number }): Square | Circle => { 34 | switch (value.type) { 35 | case 'circle': return new Circle(value.r) 36 | case 'square': return new Square(value.w) 37 | default: 38 | throw new TypeError(`Don't know how to construct ${inspect(value)}.`) 39 | } 40 | } 41 | 42 | test('instance', () => { 43 | $.instance(Circle)(shapeOf({ type: 'circle', r: 3 })) 44 | $.instance(Shape)(shapeOf({ type: 'circle', r: 1 })) 45 | expect(() => $.instance(Circle)(shapeOf({ type: 'square', w: 1 }))) 46 | .toThrow('Expected Square { w: 1 } to be instance of [class Circle extends Shape].') 47 | }) 48 | -------------------------------------------------------------------------------- /src/and.test.ts: -------------------------------------------------------------------------------- 1 | import * as $ from './index.js' 2 | 3 | test('and', () => { 4 | const r = $.and( 5 | $.object({ firstName: $.nullishOr($.string), lastName: $.nullishOr($.string), name: $.unknown }), 6 | $.object({ name: $.string }) 7 | )(JSON.parse('{"name":"Foo","role":"admin"}')) 8 | const name: string = r.name 9 | expect(name).toEqual('Foo') 10 | }) 11 | 12 | test('parametric', () => { 13 | const known = { 14 | string: $.string, 15 | number: $.number 16 | } as const 17 | const assertType = $.oneOf( 18 | 'string', 19 | 'number' 20 | ) 21 | const assertUnknown = $.exact({ 22 | type: assertType, 23 | value: $.unknown 24 | }) 25 | const assert = 26 | (type: T) => 27 | $.and( 28 | assertUnknown, 29 | value => 30 | $.object({ 31 | type: $.eq(type ?? value.type), 32 | value: known[type ?? value.type] 33 | })(value) 34 | ) 35 | assert('number')(JSON.parse('{"type":"number","value":42}')) 36 | expect(() => assert('number')(JSON.parse('{"type":"number","value":"42"}'))).toThrow() 37 | assert('string')(JSON.parse('{"type":"string","value":"42"}')) 38 | expect(() => assert('string')(JSON.parse('{"type":"string","value":42}'))).toThrow() 39 | }) 40 | -------------------------------------------------------------------------------- /src/prelude.ts: -------------------------------------------------------------------------------- 1 | /** Helper no-op, parametric type to expand intellisense for alias types. */ 2 | export type Alias = 3 | T extends infer U ? 4 | { [K in keyof U]: U[K] } : 5 | never 6 | 7 | export type Assert = 8 | (value: unknown) => 9 | R 10 | 11 | export type Asserted = 12 | A extends (value: unknown) => infer U ? 13 | U : 14 | never 15 | 16 | export type Awaited = 17 | T extends PromiseLike ? 18 | Awaited : 19 | T 20 | 21 | export type Keyed = 22 | Record 23 | 24 | /** @deprecated use {@link Nullish} */ 25 | export type NilOr = 26 | | undefined 27 | | null 28 | | T 29 | 30 | export type Nullish = 31 | | undefined 32 | | null 33 | | T 34 | 35 | export type UndefinedOr = 36 | | undefined 37 | | T 38 | 39 | export type Nullable = 40 | | null 41 | | T 42 | 43 | export type Defined = 44 | Exclude 45 | 46 | export type NonNullish = 47 | Exclude 48 | 49 | export type Predicate = 50 | (value: T) => boolean 51 | 52 | export type Primitive = 53 | | undefined 54 | | null 55 | | boolean 56 | | number 57 | | string 58 | | symbol 59 | | bigint 60 | 61 | export type Exact = 62 | T extends I ? 63 | Exclude extends never ? 64 | T : 65 | never : 66 | never 67 | 68 | export type OptionalIfUndefinedOnly = { 69 | [K in keyof T as undefined extends T[K] ? K : never]?: T[K] 70 | } 71 | 72 | export type OptionalIfUndefined = 73 | Omit> & OptionalIfUndefinedOnly 74 | 75 | export type IntersectionOfUnion = 76 | (T extends unknown ? (_: T) => unknown : never) extends (_: infer R) => unknown ? R : never 77 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import and from './and.js' 2 | import array from './array.js' 3 | import boolean from './boolean.js' 4 | import dateString from './date-string.js' 5 | import defined from './defined.js' 6 | import eq from './eq.js' 7 | import errorOf from './error-of.js' 8 | import eventually from './eventually.js' 9 | import exact from './exact.js' 10 | import false_ from './false.js' 11 | import finite from './finite.js' 12 | import gt from './gt.js' 13 | import gte from './gte.js' 14 | import if_ from './if.js' 15 | import ignore from './ignore.js' 16 | import implies from './implies.js' 17 | import in_ from './in.js' 18 | import instance from './instance.js' 19 | import keyed from './keyed.js' 20 | import lt from './lt.js' 21 | import lte from './lte.js' 22 | import never_ from './never.js' 23 | import nonBlankString from './non-blank-string.js' 24 | import nonNullish from './non-nullish.js' 25 | import null_ from './null.js' 26 | import nullish from './nullish.js' 27 | import nullishOr from './nullish-or.js' 28 | import nullOr from './null-or.js' 29 | import number from './number.js' 30 | import object from './object.js' 31 | import ok from './ok.js' 32 | import oneOf from './one-of.js' 33 | import or from './or.js' 34 | import partial from './partial.js' 35 | import positive from './positive.js' 36 | import predicate from './predicate.js' 37 | import record from './record.js' 38 | import regexp from './regexp.js' 39 | import rethrow from './rethrow.js' 40 | import safeInteger from './safe-integer.js' 41 | import sequence from './sequence.js' 42 | import strftime from './strftime.js' 43 | import string from './string.js' 44 | import true_ from './true.js' 45 | import tuple from './tuple.js' 46 | import undefined_ from './undefined.js' 47 | import undefinedOr from './undefined-or.js' 48 | import unique from './unique.js' 49 | import unknown from './unknown.js' 50 | 51 | export * from './prelude.js' 52 | 53 | export { 54 | and, 55 | array, 56 | boolean, 57 | dateString, 58 | defined, 59 | eq, 60 | errorOf, 61 | eventually, 62 | exact, 63 | false_ as false, 64 | finite, 65 | gt, 66 | gte, 67 | if_ as if, 68 | ignore, 69 | implies, 70 | in_ as in, 71 | instance, 72 | keyed, 73 | lt, 74 | lte, 75 | never_ as never, 76 | nonBlankString, 77 | nonNullish, 78 | null_ as null, 79 | nullish, 80 | nullishOr, 81 | nullOr, 82 | number, 83 | object, 84 | ok, 85 | oneOf, 86 | or, 87 | partial, 88 | positive, 89 | predicate, 90 | record, 91 | regexp, 92 | rethrow, 93 | safeInteger, 94 | sequence, 95 | strftime, 96 | string, 97 | true_ as true, 98 | tuple, 99 | undefined_ as undefined, 100 | undefinedOr, 101 | unique, 102 | unknown, 103 | 104 | // Backward compatibility: 105 | nullish as nil, 106 | nullishOr as nilOr 107 | } 108 | -------------------------------------------------------------------------------- /src/strftime.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from 'util' 2 | import type { Assert, Keyed } from './prelude.js' 3 | 4 | const map: Keyed = { 5 | 6 | // Day 7 | a: /^(Sun|Mon|Tue|Wed|Thu|Fri|Sat)/, 8 | A: /^(Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday)/, 9 | d: /^(0[1-9]|[12][0-9]|3[01])/, // 01..31 10 | e: /^( [1-9]|[12][0-9]|3[01])/, // _1..31 11 | j: /^([0-2][0-9]{2}|3[0-5][0-9]|36[0-6])/, 12 | u: /^[1-7]/, // 1..7 13 | w: /^[0-6]/, // 0..6 14 | 15 | // Week 16 | U: /^(0[0-9]|[1-4][0-9]|5[0-3])/, // 00..53 17 | V: /^(0[1-9]|[1-4][0-9]|5[0-3])/, // 01..53 18 | W: /^(0[0-9]|[1-4][0-9]|5[0-3])/, // 00..53 19 | 20 | // Month 21 | b: /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/, 22 | B: /^(January|February|March|April|May|June|July|August|September|October|November|December)/, 23 | h: /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)/, 24 | m: /^(0[1-9]|1[0-2])/, // 01..12 25 | 26 | // Year 27 | C: /^[0-9]{2}/, // 00..99 28 | g: /^[0-9]{2}/, // 00..99 29 | G: /^[0-9]{4}/, // 0000..9999 30 | y: /^[0-9]{2}/, // 00..99 31 | Y: /^[0-9]{4}/, // 0000..9999 32 | 33 | // Time 34 | H: /^(0[0-9]|1[0-9]|2[0-3])/, // 00..23 35 | k: /^( [0-9]|1[0-9]|2[0-3])/, // _0..23 36 | I: /^(0[1-9]|1[0-2])/, // 01..12 37 | l: /^( [1-9]|1[0-2])/, // _1..12 38 | M: /^[0-5][0-9]/, // 00..59 39 | p: /^(AM|PM)/, 40 | P: /^(am|pm)/, 41 | r: '%I:%M:%S %p', 42 | R: '%H:%M', 43 | S: /^[0-5][0-9]/, 44 | T: '%H:%M:%S', 45 | X: '%H:%M:%S', 46 | z: /^[+-][0-9]{4}/, 47 | Z: /^[A-Z]+/, 48 | 49 | // Time and Date Stamps 50 | // %c not supported. 51 | D: '%m/%d/%y', 52 | F: '%Y-%m-%d', 53 | s: /^[0-9]+/, 54 | // %x not supported. 55 | 56 | // Misc 57 | n: /^\n/, 58 | t: /^\t/, 59 | '%': /^%/ 60 | } 61 | 62 | type Token = 63 | | { type: 'regexp', value: RegExp } 64 | | { type: 'literal', value: string } 65 | 66 | const tokens = 67 | function *(value: string): Generator { 68 | let j = 0 69 | let i = 0 70 | for (i = 0; i < value.length; i++) { 71 | if (value[i] === '%') { 72 | if (j < i - 1) { 73 | yield { type: 'literal', value: value.slice(j + 1, i) } 74 | } 75 | const r = map[value[i + 1]] 76 | if (typeof r === 'undefined') { 77 | throw new TypeError(`Unknown strftime rule %${value[i + 1]}.`) 78 | } 79 | if (typeof r === 'string') { 80 | yield *tokens(r) 81 | } 82 | if (r instanceof RegExp) { 83 | yield { type: 'regexp', value: r } 84 | } 85 | i++ 86 | j = i 87 | } 88 | } 89 | if (j < i - 1) { 90 | yield { type: 'literal', value: value.slice(j + 1, i) } 91 | } 92 | } 93 | 94 | const strftime = 95 | (f: string): Assert => { 96 | if (typeof f !== 'string') { 97 | throw new TypeError(`Expected format string, got ${inspect(f)}.`) 98 | } 99 | return (value: unknown) => { 100 | if (typeof value !== 'string') { 101 | throw new TypeError(`Expected ${inspect(f)} strftime formatted string, got ${inspect(value)}.`) 102 | } 103 | let i = 0 104 | for (const token of tokens(f)) { 105 | const value_ = value.substr(i) 106 | switch (token.type) { 107 | case 'regexp': { 108 | const m = value_.match(token.value) 109 | if (m) { 110 | i += m[0].length 111 | } else { 112 | throw new TypeError(`Expected ${inspect(f)} strftime format, got ${inspect(value)}, failed at index ${i}, unrecognised part ${inspect(value_)}.`) 113 | } 114 | break 115 | } 116 | case 'literal': 117 | if (!value_.startsWith(token.value)) { 118 | throw new TypeError(`Expected ${inspect(f)} strftime format, got ${inspect(value)}, failed at index ${i}, unrecognised part ${inspect(value_)}.`) 119 | } 120 | i += token.value.length 121 | break 122 | } 123 | } 124 | if (i !== value.length) { 125 | throw new TypeError(`Expected ${inspect(f)} strftime format, got ${inspect(value)}, failed at index ${i}, unrecognised part ${inspect(value.substr(i))}.`) 126 | } 127 | return value 128 | } 129 | } 130 | 131 | export default strftime 132 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ![@appliedblockchain/tsql](https://shields.io/npm/v/@appliedblockchain/assert-combinators) 2 | ![esm cjs](https://img.shields.io/badge/module-esm%20cjs-f39f37) 3 | 4 | --- 5 | 6 | ## Summary 7 | 8 | Functional assertion combinators. 9 | 10 | Name | Type | Runtime 11 | --------------------------|-------------------------------------------------|------------------------------ 12 | Primitive | `string` | `$.string` 13 | Primitive | `number` | `$.number` 14 | Primitive | `boolean` | `$.boolean` 15 | Primitive | `undefined` | `$.undefined` 16 | Primitive | `null` | `$.null` 17 | Undefined or null | `undefined \| null` | `$.nil` 18 | Undefined or A | `undefined \| A` | `$.undefinedOr(a)` 19 | Nullable A | `null \| A` | `$.nullOr(a)` 20 | Nillable A | `undefined \| null \| A` | `$.nilOr(a)` 21 | Unknown | `unknown` | `$.unknown` 22 | Array | `A[]` | `$.array(a)` 23 | Object | `{ a: A, b: B }` | `$.object({ a, b })` 24 | Exact object | `{ a: A, b: B }` | `$.exact({ a, b })` 25 | Record | `Record` | `$.record(k, v)` 26 | Keyed object | `Record` | `$.keyed(v)` 27 | Intersection | `A & B` | `$.and(a, b)` 28 | Primitive type union | `'A' \| 'B'` | `$.oneOf('A', 'B')` 29 | Union | `A \| B` | `$.or(a, b)` 30 | Date string `YYYY-MM-DD` | `string`[weaker](#not-precise-types) | `$.dateString` 31 | Defined | `Exclude` | `$.defined(a)` 32 | Literal primitive | `"foo"`, `42` | `$.eq('foo')`, `$.eq(42)` 33 | Tuple | `[number, string]` | `$.tuple($.number, $.string)` 34 | Finite number | `number`[weaker](#not-precise-types) | `$.finite` 35 | Positive number | `number`[weaker](#not-precise-types) | `$.positive` 36 | Safe integer | `number`[weaker](#not-precise-types) | `$.safeInteger` 37 | Greater than | `number`[weaker](#not-precise-types) | `$.gt(42)` 38 | Greater than | `number`[weaker](#not-precise-types) | `$.gt(42)` 39 | Greater or equal than | `number`[weaker](#not-precise-types) | `$.gte(42)` 40 | Non blank string | `string`[weaker](#not-precise-types) | `$.nonBlankString` 41 | Regexp | `string`[weaker](#not-precise-types) | `$.regexp(/^[a-z]+$/i)` 42 | Strftime formatted string | `string`[weaker](#not-precise-types) | `$.strftime('%Y-%m-%d')` 43 | 44 | ## Utility functions 45 | 46 | * errorOf – instead of throwing, returns Error or undefined if assertion passes. 47 | * identity 48 | * if 49 | * implies 50 | * in 51 | * predicate 52 | * rethrow 53 | 54 | ## Examples 55 | 56 | ```ts 57 | import * as $ from '@appliedblockchain/assert-combinators' 58 | 59 | ws.on('message', _ => { 60 | 61 | const { method, agree } = $.object({ 62 | method: $.string, 63 | agree: $.boolean 64 | })(JSON.parse(_)) 65 | 66 | // Types are correct: 67 | // method: string 68 | // agree: boolean 69 | 70 | }) 71 | ``` 72 | 73 | ## Not precise types 74 | 75 | In some cases runtime type assertions provide stronger guarantees than static types. 76 | 77 | For example `$.finite` asserts that the value is not only a `number` but also that is not `NaN`, `Infinity` or `-Infinity`. 78 | 79 | [Opaque types](#opaque-types) section provides solution for some cases. 80 | 81 | ## Opaque types 82 | 83 | Unlike Flow, TypeScript doesn't directly support opaque types. 84 | 85 | However, they can be emulated by intersecting with object containing unique property type which exists in static type system only. It does not exist in runtime value. 86 | 87 | ```ts 88 | type Finite = number & { readonly _tag: 'Finite' } 89 | ``` 90 | 91 | Opaque types allow to design code in such a way that value of the type can be created in one place – as result of runtime type assertion – only. The only possible way of creating values of this type is to create valid values. Those assertions have to happen at construction and I/O boundaries only. Once value is validated, it enters static type system. It doesn't have to be re-validated anywhere else in the code. Usage of the value is safe, guaranteed to conform to this assertion. 92 | 93 | Good examples of opaque type candidates are `NonEmptyArray`, `Positive`, `Email`. 94 | `ValidatedEmail` – ie. an email that passed some async validation can be used to annotate function parameter for functions that should be used only for validated emails – without the need for re-validating email in each function's body. 95 | 96 | ## Optional tuple tail 97 | 98 | When tail of tuple accepts undefined values, resulting tuple may have shorter length than arity of assertion function. 99 | 100 | ```ts 101 | const assertMyTuple = $.tuple($.string, $.undefinedOr($.number)) 102 | assertMyTuple([ 'foo' ]) // ok 103 | assertMyTuple([ 'foo', 1 ]) // ok 104 | ``` 105 | 106 | A good rule of thumb is to destructure tuple elements if it accepts undefined at tail position to make sure the code doesn't rely on the length, ie: 107 | 108 | ```ts 109 | const [ myString, maybeNumber ] = assertMyTuple(input) 110 | ``` 111 | 112 | ## Exhaustive switch 113 | 114 | Use `never` assertion to force exhaustiveness on switch statements: 115 | 116 | ```ts 117 | import * as $ from '@appliedblockchain/assert-combinators' 118 | 119 | type X = 'a' | 'b' | 'c' 120 | 121 | const assertX: $.Assert = 122 | $.oneOf('a', 'b', 'c') 123 | 124 | const x: X = assertX(JSON.parse('"c"')) 125 | switch (x) { 126 | case 'a': 127 | case 'b': 128 | console.log(x) 129 | break 130 | default: 131 | 132 | // Argument of type 'string' is not assignable to parameter of type 'never'.ts(2345) 133 | // const x: "c" 134 | $.never(x) 135 | } 136 | ``` 137 | 138 | ## License 139 | 140 | MIT License 141 | 142 | Copyright 2019 Applied Blockchain 143 | 144 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 145 | 146 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 147 | 148 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 149 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v5.7.0](../../compare/v5.6.1...v5.7.0) (2024-01-29) 4 | 5 | * Fix(and): fixing typing for and. 6 | * Chore(deps): bumping deps. 7 | * Updating changelog. 8 | 9 | ## [v5.6.1](../../compare/v5.6.0...v5.6.1) (2023-10-05) 10 | 11 | * Fix(or): fixing typing for `or`. 12 | * Updating changelog. 13 | 14 | ## [v5.6.0](../../compare/v5.5.0...v5.6.0) (2022-12-14) 15 | 16 | * Feat: optional if undefined. 17 | * Updating changelog. 18 | 19 | ## [v5.5.0](../../compare/v5.4.1...v5.5.0) (2022-12-14) 20 | 21 | * Chore: bumping ts. 22 | * Updating changelog. 23 | 24 | ## [v5.4.1](../../compare/v5.4.0...v5.4.1) (2022-09-21) 25 | 26 | * Fix: preserve backward compatibility. 27 | * Updating changelog. 28 | 29 | ## [v5.4.0](../../compare/v5.3.2...v5.4.0) (2022-09-21) 30 | 31 | * Feat(eventually): custom error constructor with last result inspection. 32 | * Chore: bumping dev deps. 33 | * Renaming nil to nullish. 34 | * Bumping deps. 35 | * Updating changelog. 36 | 37 | ## [v5.3.2](../../compare/v5.3.1...v5.3.2) (2022-03-28) 38 | 39 | * Fixing types. 40 | * Updating changelog. 41 | 42 | ## [v5.3.1](../../compare/v5.3.0...v5.3.1) (2022-03-28) 43 | 44 | * Fixing exact type. 45 | * Updating changelog. 46 | 47 | ## [v5.3.0](../../compare/v5.2.0...v5.3.0) (2022-03-28) 48 | 49 | * Allow primitives in objects. 50 | * Updating changelog. 51 | 52 | ## [v5.2.0](../../compare/v5.1.1...v5.2.0) (2022-03-28) 53 | 54 | * Adding true and false. 55 | * Updating changelog. 56 | 57 | ## [v5.1.1](../../compare/v5.1.0...v5.1.1) (2022-03-28) 58 | 59 | * Fixing result type in eventually. 60 | * Updating changelog. 61 | 62 | ## [v5.1.0](../../compare/v5.0.0...v5.1.0) (2022-03-28) 63 | 64 | * Adding eventually. 65 | * Adding non-nullish. 66 | * Updating changelog. 67 | 68 | ## [v5.0.0](../../compare/v4.5.0...v5.0.0) (2022-02-25) 69 | 70 | * Meta cleanups. 71 | * Universal mjs/cjs. 72 | * Create codeql-analysis.yml. 73 | * Updating changelog. 74 | 75 | ## [v4.5.0](../../compare/v4.4.0...v4.5.0) (2021-12-13) 76 | 77 | * Bumping node v14 to v16; bumping deps. 78 | * Updating changelog. 79 | 80 | ## [v4.4.0](../../compare/v4.3.0...v4.4.0) (2021-10-16) 81 | 82 | * Bumping deps. 83 | * Publish post version. 84 | * Fixing typos. 85 | * Updating changelog. 86 | 87 | ## [v4.3.0](../../compare/v4.2.0...v4.3.0) (2021-09-03) 88 | 89 | * Bumping deps. 90 | * Updating changelog. 91 | 92 | ## [v4.2.0](../../compare/v4.1.3...v4.2.0) (2021-08-12) 93 | 94 | * Adding never for exhaustive switch. 95 | * Updating changelog. 96 | 97 | ## [v4.1.3](../../compare/v4.1.2...v4.1.3) (2021-06-18) 98 | 99 | * Fix: don't use private interfaces. 100 | * Updating changelog. 101 | 102 | ## [v4.1.2](../../compare/v4.1.1...v4.1.2) (2021-06-17) 103 | 104 | * Be nice to old parsers. 105 | * Updating changelog. 106 | 107 | ## [v4.1.1](../../compare/v4.1.0...v4.1.1) (2021-06-16) 108 | 109 | * Using noImplicitAny. 110 | * Updating changelog. 111 | 112 | ## [v4.1.0](../../compare/v4.0.0...v4.1.0) (2021-06-16) 113 | 114 | * Adding ignore. 115 | * Updating changelog. 116 | 117 | ## [v4.0.0](../../compare/v3.4.0...v4.0.0) (2021-06-16) 118 | 119 | * Updating tuple. 120 | * Test on prepublishOnly. 121 | * Bumping deps. 122 | * Simplifying object type. 123 | * Simplifying exact types. 124 | * Dropping rethrow helper. 125 | * Dropping clone, map, error-message. Adding error-of. Eq on primitive. 126 | * Dropping default export, fixing partial. 127 | * Cleanups. 128 | * Adding asserted. 129 | * Assert primitive type union in one-of. 130 | * Updating changelog. 131 | 132 | ## [v3.4.0](../../compare/v3.3.2...v3.4.0) (2021-05-20) 133 | 134 | * Node 14 target, bumping deps. 135 | * Adding import. 136 | * Updating changelog. 137 | 138 | ## [v3.3.2](../../compare/v3.3.1...v3.3.2) (2021-05-19) 139 | 140 | * Adding record. 141 | * Updating readme. 142 | * Updating changelog. 143 | 144 | ## [v3.3.1](../../compare/v3.3.0...v3.3.1) (2021-03-08) 145 | 146 | * Use isolated modules. 147 | * Updating changelog. 148 | 149 | ## [v3.3.0](../../compare/v3.2.0...v3.3.0) (2021-03-05) 150 | 151 | * Adding positive, bumping npms, exporting utility types. 152 | * Updating changelog. 153 | 154 | ## [v3.2.0](../../compare/v3.1.3...v3.2.0) (2020-10-14) 155 | 156 | * Adding strftime. 157 | * Updating changelog. 158 | 159 | ## [v3.1.3](../../compare/v3.1.2...v3.1.3) (2020-10-09) 160 | 161 | * Renaming expand to alias. 162 | * Updating changelog. 163 | 164 | ## [v3.1.2](../../compare/v3.1.1...v3.1.2) (2020-10-09) 165 | 166 | * Adding expand type. 167 | * Updating changelog. 168 | 169 | ## [v3.1.1](../../compare/v3.1.0...v3.1.1) (2020-10-09) 170 | 171 | * Adding export. 172 | * Adding .github to npm ignore. 173 | * Updating changelog. 174 | 175 | ## [v3.1.0](../../compare/v3.0.1...v3.1.0) (2020-10-03) 176 | 177 | * Use unknown as default type parameter for keyed. 178 | * Check for array in keyed. 179 | * Renaming kv to keyed. 180 | * Adding async-return-type utility type. 181 | * Adding awaited utility type. 182 | * Adding kv and null-or, undefined-or utility types. 183 | * Updating changelog. 184 | 185 | ## [v3.0.1](../../compare/v3.0.0...v3.0.1) (2020-09-29) 186 | 187 | * Minor updates. 188 | * Updating changelog. 189 | 190 | ## [v3.0.0](../../compare/v2.5.0...v3.0.0) (2020-09-16) 191 | 192 | * Bumping npms. 193 | * Inline object and exact result so vscode expands type on hover... 194 | * Minor fixes. 195 | * Minor fixes. 196 | * Adding vscode settings. 197 | * Moving to ts. 198 | * Updating changelog. 199 | 200 | ## [v2.5.0](../../compare/v2.4.0...v2.5.0) (2020-06-28) 201 | 202 | * Adding error-message test. 203 | * Adding error-message combinator. 204 | * Bumping npms. 205 | * Updating changelog. 206 | 207 | ## [v2.4.0](../../compare/v2.3.0...v2.4.0) (2020-05-16) 208 | 209 | * Adding instance. 210 | * Updating changelog. 211 | 212 | ## [v2.3.0](../../compare/v2.2.0...v2.3.0) (2020-05-13) 213 | 214 | * Adding indexer. 215 | * Updating changelog. 216 | 217 | ## [v2.2.0](../../compare/v2.1.0...v2.2.0) (2020-05-12) 218 | 219 | * Adding tests. 220 | * Updating index. 221 | * Bumping npms. 222 | * Adding info on optionals in error message. 223 | * Adding rethrow. 224 | * Adding assert. 225 | * Updating changelog. 226 | 227 | ## [v2.1.0](../../compare/v2.0.0...v2.1.0) (2020-04-29) 228 | 229 | * Updating npms. 230 | * Adding eq, safe-integer and non-blank string to index. 231 | * Updating unique type. 232 | * Adding safe-integer. 233 | * Adding non-blank-string. 234 | * Adding eq. 235 | * Updating changelog. 236 | 237 | ## [v2.0.0](../../compare/v1.6.0...v2.0.0) (2020-04-18) 238 | 239 | * Using unique with optional map function. 240 | * Updating changelog. 241 | 242 | ## [v1.6.0](../../compare/v1.5.0...v1.6.0) (2020-04-18) 243 | 244 | * Adding unique. 245 | * Updating changelog. 246 | 247 | ## [v1.5.0](../../compare/v1.4.0...v1.5.0) (2020-04-15) 248 | 249 | * Adding if, implies, ok; exporting all. 250 | * Updating changelog. 251 | 252 | ## [v1.4.0](../../compare/v1.3.0...v1.4.0) (2020-04-15) 253 | 254 | * Adding regexp. 255 | * Updating changelog. 256 | 257 | ## [v1.3.0](../../compare/v1.2.1...v1.3.0) (2020-04-14) 258 | 259 | * Exporting sequence and map, fixing one-of. 260 | * Updating changelog. 261 | 262 | ## [v1.2.1](../../compare/v1.2.0...v1.2.1) (2020-04-14) 263 | 264 | * Updating type on one-of. 265 | * Updating changelog. 266 | 267 | ## [v1.2.0](../../compare/v1.1.1...v1.2.0) (2020-04-13) 268 | 269 | * Making one-of strict. 270 | * Export one-of. 271 | * Don't collide with mixed. 272 | * Adding one-of. 273 | * Adding map. 274 | * Adding in. 275 | * Adding clone. 276 | * Adding sequence. 277 | * Bumping flow. 278 | * Updating changelog. 279 | 280 | ## [v1.1.1](../../compare/v1.1.0...v1.1.1) (2020-04-02) 281 | 282 | * Fixing flow errors. 283 | * Updating changelog. 284 | 285 | ## [v1.1.0](../../compare/v1.0.0...v1.1.0) (2020-04-02) 286 | 287 | * Adding predicate. Using flow strict mode. 288 | * Adding nil, null and undefined. 289 | * Including nested keys in error message. Don't enforce read only array. 290 | * Updating changelog. 291 | 292 | ## [v1.0.0](../../compare/v0.0.3...v1.0.0) (2020-03-31) 293 | 294 | * Multiple updates. 295 | * Adding gt, gte, lt and lte. 296 | * Adding defined. 297 | * Updating changelog. 298 | 299 | ## [v0.0.3](../../compare/v0.0.2...v0.0.3) (2020-03-12) 300 | 301 | * Adding shape. 302 | * Adding tuple. 303 | * Ignore libdef for jest. 304 | * Updating changelog. 305 | --------------------------------------------------------------------------------