├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── dev-only.js ├── example.js ├── license ├── media ├── logo.png ├── logo.svg └── readme.md ├── package.json ├── readme.md ├── source ├── argument-error.ts ├── index.ts ├── modifiers.ts ├── operators │ └── not.ts ├── predicates.ts ├── predicates │ ├── any.ts │ ├── array-buffer.ts │ ├── array.ts │ ├── base-predicate.ts │ ├── bigint.ts │ ├── boolean.ts │ ├── data-view.ts │ ├── date.ts │ ├── error.ts │ ├── map.ts │ ├── number.ts │ ├── object.ts │ ├── predicate.ts │ ├── set.ts │ ├── string.ts │ ├── typed-array.ts │ ├── weak-map.ts │ └── weak-set.ts ├── test.ts ├── typed-array.ts └── utils │ ├── generate-argument-error-message.ts │ ├── generate-stack.ts │ ├── has-items.ts │ ├── infer-label.browser.ts │ ├── infer-label.ts │ ├── match-shape.ts │ ├── of-type-deep.ts │ ├── of-type.ts │ └── random-id.ts ├── test ├── any-multiple-errors.ts ├── any.ts ├── array-buffer.ts ├── array.ts ├── bigint.ts ├── boolean.ts ├── buffer.ts ├── custom-message.ts ├── custom-predicate.ts ├── data-view.ts ├── date.ts ├── error.ts ├── fixtures │ └── create-error.ts ├── function.ts ├── infer-label.ts ├── iterable.ts ├── map.ts ├── nan.ts ├── null-or-undefined.ts ├── null.ts ├── number.ts ├── object.ts ├── optional.ts ├── promise.ts ├── regexp.ts ├── set.ts ├── string.ts ├── symbol.ts ├── test.ts ├── typed-array.ts ├── types.ts ├── undefined.ts ├── weak-map.ts └── weak-set.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 22 14 | - 18 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install --force 21 | - run: npm test 22 | - uses: codecov/codecov-action@v4 23 | if: matrix.node-version == 18 24 | with: 25 | token: ${{ secrets.CODECOV_TOKEN }} 26 | fail_ci_if_error: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | .nyc_output 4 | coverage 5 | dist 6 | docs 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /dev-only.js: -------------------------------------------------------------------------------- 1 | let ow; 2 | if (process.env.NODE_ENV === 'production') { 3 | const shim = new Proxy((() => {}), { 4 | get: () => shim, 5 | apply: () => shim, 6 | }); 7 | 8 | ow = shim; 9 | } else { 10 | ow = await import('./dist/index.js'); 11 | } 12 | 13 | export default ow; 14 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | import ow from './dist/index.js' 2 | 3 | const logError = fn => { 4 | try { 5 | fn(); 6 | } catch (error) { 7 | console.log(error.message); 8 | } 9 | }; 10 | 11 | const fn = input => { 12 | ow(input, ow.string.minLength(10)); 13 | }; 14 | 15 | logError(() => { 16 | fn(10); 17 | }); 18 | 19 | logError(() => { 20 | fn('foo'); 21 | }); 22 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sindresorhus/ow/2b7fedac1d86d0827a657109c633286890dc8eeb/media/logo.png -------------------------------------------------------------------------------- /media/readme.md: -------------------------------------------------------------------------------- 1 | # Media 2 | 3 | ## Logo 4 | 5 | Logo is based on [Comic Book Elements](https://creativemarket.com/swedishpoints/232087-Comic-Book-Elements) by Carl Eriksson. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ow", 3 | "version": "2.0.0", 4 | "description": "Function argument validation for humans", 5 | "license": "MIT", 6 | "repository": "sindresorhus/ow", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | ".": { 16 | "types": "./dist/index.d.ts", 17 | "default": "./dist/index.js" 18 | }, 19 | "./dev-only": { 20 | "types": "./dist/index.d.ts", 21 | "default": "./dev-only.js" 22 | } 23 | }, 24 | "sideEffects": false, 25 | "engines": { 26 | "node": ">=18" 27 | }, 28 | "scripts": { 29 | "test": "xo && NODE_OPTIONS='--import=tsx/esm' c8 ava", 30 | "docs": "typedoc source/index.ts", 31 | "build": "del-cli dist && tsc", 32 | "prepare": "npm run build", 33 | "postpublish": "npm run docs && gh-pages --dist docs --no-history --message \"Deploy documentation\"", 34 | "example": "npm run build && node example.js" 35 | }, 36 | "files": [ 37 | "dist", 38 | "dev-only.js" 39 | ], 40 | "keywords": [ 41 | "type", 42 | "types", 43 | "check", 44 | "checking", 45 | "guard", 46 | "guards", 47 | "assert", 48 | "assertion", 49 | "predicate", 50 | "predicates", 51 | "is", 52 | "validate", 53 | "validation", 54 | "utility", 55 | "util", 56 | "typeof", 57 | "instanceof", 58 | "object" 59 | ], 60 | "dependencies": { 61 | "@sindresorhus/is": "^6.3.0", 62 | "callsites": "^4.1.0", 63 | "dot-prop": "^8.0.2", 64 | "environment": "^1.0.0", 65 | "fast-equals": "^5.0.1", 66 | "is-identifier": "^1.0.0" 67 | }, 68 | "devDependencies": { 69 | "@sindresorhus/tsconfig": "^5.0.0", 70 | "@types/node": "^20.12.8", 71 | "ava": "^6.1.2", 72 | "c8": "^9.1.0", 73 | "del-cli": "^5.1.0", 74 | "expect-type": "^0.19.0", 75 | "gh-pages": "^6.1.1", 76 | "tsx": "^4.9.1", 77 | "typedoc": "^0.25.13", 78 | "typescript": "^5.4.5", 79 | "xo": "^0.58.0" 80 | }, 81 | "browser": { 82 | "./dist/utils/infer-label.js": "./dist/utils/infer-label.browser.js" 83 | }, 84 | "xo": { 85 | "ignores": [ 86 | "example.js", 87 | "dev-only.js", 88 | "source/utils/infer-label.browser.ts" 89 | ], 90 | "rules": { 91 | "no-useless-return": "off", 92 | "@typescript-eslint/explicit-function-return-type": "error", 93 | "@typescript-eslint/ban-types": "off", 94 | "@typescript-eslint/no-explicit-any": "off", 95 | "@typescript-eslint/no-empty-function": "off", 96 | "@typescript-eslint/restrict-template-expressions": "off", 97 | "@typescript-eslint/no-unsafe-return": "off", 98 | "@typescript-eslint/no-unsafe-argument": "off" 99 | } 100 | }, 101 | "ava": { 102 | "files": [ 103 | "test/**", 104 | "!test/fixtures/**" 105 | ], 106 | "extensions": { 107 | "ts": "module" 108 | }, 109 | "workerThreads": false 110 | }, 111 | "c8": { 112 | "reporter": [ 113 | "text", 114 | "lcov" 115 | ] 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 |
5 |

6 | 7 | [![Coverage Status](https://codecov.io/gh/sindresorhus/ow/branch/main/graph/badge.svg)](https://codecov.io/gh/sindresorhus/ow) 8 | 9 | > Function argument validation for humans 10 | 11 | For schema validation, I recommend [`zod`](https://github.com/colinhacks/zod). 12 | 13 | ## Highlights 14 | 15 | - Expressive chainable API 16 | - Lots of built-in validations 17 | - Supports custom validations 18 | - Automatic label inference in Node.js 19 | - Written in TypeScript 20 | 21 | ## Install 22 | 23 | ```sh 24 | npm install ow 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```ts 30 | import ow from 'ow'; 31 | 32 | const unicorn = input => { 33 | ow(input, ow.string.minLength(5)); 34 | 35 | // … 36 | }; 37 | 38 | unicorn(3); 39 | //=> ArgumentError: Expected `input` to be of type `string` but received type `number` 40 | 41 | unicorn('yo'); 42 | //=> ArgumentError: Expected string `input` to have a minimum length of `5`, got `yo` 43 | ``` 44 | 45 | We can also match the shape of an object. 46 | 47 | ```ts 48 | import ow from 'ow'; 49 | 50 | const unicorn = { 51 | rainbow: '🌈', 52 | stars: { 53 | value: '🌟' 54 | } 55 | }; 56 | 57 | ow(unicorn, ow.object.exactShape({ 58 | rainbow: ow.string, 59 | stars: { 60 | value: ow.number 61 | } 62 | })); 63 | //=> ArgumentError: Expected property `stars.value` to be of type `number` but received type `string` in object `unicorn` 64 | ``` 65 | 66 | ***Note:*** If you intend on using `ow` for development purposes only, use `import ow from 'ow/dev-only'` instead of the usual `import ow from 'ow'`, and run the bundler with `NODE_ENV` set to `production` (e.g. `$ NODE_ENV="production" parcel build index.js`). This will make `ow` automatically export a shim when running in production, which should result in a significantly lower bundle size. 67 | 68 | ## API 69 | 70 | [Complete API documentation](https://sindresorhus.com/ow/) 71 | 72 | Ow includes TypeScript type guards, so using it will narrow the type of previously-unknown values. 73 | 74 | ```ts 75 | function (input: unknown) { 76 | input.slice(0, 3) // Error, Property 'slice' does not exist on type 'unknown' 77 | 78 | ow(input, ow.string) 79 | 80 | input.slice(0, 3) // OK 81 | } 82 | ``` 83 | 84 | ### ow(value, predicate) 85 | 86 | Test if `value` matches the provided `predicate`. Throws an `ArgumentError` if the test fails. 87 | 88 | ### ow(value, label, predicate) 89 | 90 | Test if `value` matches the provided `predicate`. Throws an `ArgumentError` with the specified `label` if the test fails. 91 | 92 | The `label` is automatically inferred in Node.js but you can override it by passing in a value for `label`. The automatic label inference doesn't work in the browser. 93 | 94 | ### ow.isValid(value, predicate) 95 | 96 | Returns `true` if the value matches the predicate, otherwise returns `false`. 97 | 98 | ### ow.create(predicate) 99 | 100 | Create a reusable validator. 101 | 102 | ```ts 103 | const checkPassword = ow.create(ow.string.minLength(6)); 104 | 105 | const password = 'foo'; 106 | 107 | checkPassword(password); 108 | //=> ArgumentError: Expected string `password` to have a minimum length of `6`, got `foo` 109 | ``` 110 | 111 | ### ow.create(label, predicate) 112 | 113 | Create a reusable validator with a specific `label`. 114 | 115 | ```ts 116 | const checkPassword = ow.create('password', ow.string.minLength(6)); 117 | 118 | checkPassword('foo'); 119 | //=> ArgumentError: Expected string `password` to have a minimum length of `6`, got `foo` 120 | ``` 121 | 122 | ### ow.any(...predicate[]) 123 | 124 | Returns a predicate that verifies if the value matches at least one of the given predicates. 125 | 126 | ```ts 127 | ow('foo', ow.any(ow.string.maxLength(3), ow.number)); 128 | ``` 129 | 130 | ### ow.optional.{type} 131 | 132 | Makes the predicate optional. An optional predicate means that it doesn't fail if the value is `undefined`. 133 | 134 | ```ts 135 | ow(1, ow.optional.number); 136 | 137 | ow(undefined, ow.optional.number); 138 | ``` 139 | 140 | ### ow.{type} 141 | 142 | All the below types return a predicate. Every predicate has some extra operators that you can use to test the value even more fine-grained. 143 | 144 | [Predicate docs.](https://sindresorhus.com/ow/types/Predicates.html) 145 | 146 | #### Primitives 147 | 148 | - `undefined` 149 | - `null` 150 | - `string` 151 | - `number` 152 | - `boolean` 153 | - `symbol` 154 | 155 | #### Built-in types 156 | 157 | - `array` 158 | - `function` 159 | - `buffer` 160 | - `object` 161 | - `regExp` 162 | - `date` 163 | - `error` 164 | - `promise` 165 | - `map` 166 | - `set` 167 | - `weakMap` 168 | - `weakSet` 169 | 170 | #### Typed arrays 171 | 172 | - `int8Array` 173 | - `uint8Array` 174 | - `uint8ClampedArray` 175 | - `int16Array` 176 | - `uint16Array` 177 | - `int32Array` 178 | - `uint32Array` 179 | - `float32Array` 180 | - `float64Array` 181 | 182 | #### Structured data 183 | 184 | - `arrayBuffer` 185 | - `dataView` 186 | - `sharedArrayBuffer` 187 | 188 | #### Miscellaneous 189 | 190 | - `nan` 191 | - `nullOrUndefined` 192 | - `iterable` 193 | - `typedArray` 194 | 195 | ### Predicates 196 | 197 | The following predicates are available on every type. 198 | 199 | #### not 200 | 201 | Inverts the following predicate. 202 | 203 | ```ts 204 | ow(1, ow.number.not.infinite); 205 | 206 | ow('', ow.string.not.empty); 207 | //=> ArgumentError: Expected string to not be empty, got `` 208 | ``` 209 | 210 | #### is(fn) 211 | 212 | Use a custom validation function. Return `true` if the value matches the validation, return `false` if it doesn't. 213 | 214 | ```ts 215 | ow(1, ow.number.is(x => x < 10)); 216 | 217 | ow(1, ow.number.is(x => x > 10)); 218 | //=> ArgumentError: Expected `1` to pass custom validation function 219 | ``` 220 | 221 | Instead of returning `false`, you can also return a custom error message which results in a failure. 222 | 223 | ```ts 224 | const greaterThan = (max: number, x: number) => { 225 | return x > max || `Expected \`${x}\` to be greater than \`${max}\``; 226 | }; 227 | 228 | ow(5, ow.number.is(x => greaterThan(10, x))); 229 | //=> ArgumentError: Expected `5` to be greater than `10` 230 | ``` 231 | 232 | #### validate(fn) 233 | 234 | Use a custom validation object. The difference with `is` is that the function should return a validation object, which allows more flexibility. 235 | 236 | ```ts 237 | ow(1, ow.number.validate(value => ({ 238 | validator: value > 10, 239 | message: `Expected value to be greater than 10, got ${value}` 240 | }))); 241 | //=> ArgumentError: (number) Expected value to be greater than 10, got 1 242 | ``` 243 | 244 | You can also pass in a function as `message` value which accepts the label as argument. 245 | 246 | ```ts 247 | ow(1, 'input', ow.number.validate(value => ({ 248 | validator: value > 10, 249 | message: label => `Expected ${label} to be greater than 10, got ${value}` 250 | }))); 251 | //=> ArgumentError: Expected number `input` to be greater than 10, got 1 252 | ``` 253 | 254 | #### message(string | fn) 255 | 256 | Provide a custom message: 257 | 258 | ```ts 259 | ow('🌈', 'unicorn', ow.string.equals('🦄').message('Expected unicorn, got rainbow')); 260 | //=> ArgumentError: Expected unicorn, got rainbow 261 | ``` 262 | 263 | You can also pass in a function which receives the value as the first parameter and the label as the second parameter and is expected to return the message. 264 | 265 | ```ts 266 | ow('🌈', ow.string.minLength(5).message((value, label) => `Expected ${label}, to have a minimum length of 5, got \`${value}\``)); 267 | //=> ArgumentError: Expected string, to be have a minimum length of 5, got `🌈` 268 | ``` 269 | 270 | It's also possible to add a separate message per validation: 271 | 272 | ```ts 273 | ow( 274 | '1234', 275 | ow.string 276 | .minLength(5).message((value, label) => `Expected ${label}, to be have a minimum length of 5, got \`${value}\``) 277 | .url.message('This is no url') 278 | ); 279 | //=> ArgumentError: Expected string, to be have a minimum length of 5, got `1234` 280 | 281 | ow( 282 | '12345', 283 | ow.string 284 | .minLength(5).message((value, label) => `Expected ${label}, to be have a minimum length of 5, got \`${value}\``) 285 | .url.message('This is no url') 286 | ); 287 | //=> ArgumentError: This is no url 288 | ``` 289 | 290 | This can be useful for creating your own reusable validators which can be extracted to a separate npm package. 291 | 292 | ### TypeScript 293 | 294 | **Requires TypeScript 4.7 or later.** 295 | 296 | Ow includes a type utility that lets you to extract a TypeScript type from the given predicate. 297 | 298 | ```ts 299 | import ow, {Infer} from 'ow'; 300 | 301 | const userPredicate = ow.object.exactShape({ 302 | name: ow.string 303 | }); 304 | 305 | type User = Infer; 306 | ``` 307 | 308 | ## Related 309 | 310 | - [@sindresorhus/is](https://github.com/sindresorhus/is) - Type check values 311 | - [ngx-ow](https://github.com/SamVerschueren/ngx-ow) - Angular form validation on steroids 312 | -------------------------------------------------------------------------------- /source/argument-error.ts: -------------------------------------------------------------------------------- 1 | import {generateStackTrace} from './utils/generate-stack.js'; 2 | 3 | const wrapStackTrace = (error: ArgumentError, stack: string): string => `${error.name}: ${error.message}\n${stack}`; 4 | 5 | /** 6 | @hidden 7 | */ 8 | export class ArgumentError extends Error { 9 | readonly validationErrors: ReadonlyMap>; 10 | 11 | constructor(message: string, context: Function, errors = new Map>()) { 12 | super(message); 13 | 14 | this.name = 'ArgumentError'; 15 | 16 | if (Error.captureStackTrace) { 17 | Error.captureStackTrace(this, context); 18 | } else { 19 | this.stack = wrapStackTrace(this, generateStackTrace()); 20 | } 21 | 22 | this.validationErrors = errors; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /source/index.ts: -------------------------------------------------------------------------------- 1 | import callsites from 'callsites'; 2 | import {inferLabel} from './utils/infer-label.js'; 3 | import {isPredicate, type BasePredicate} from './predicates/base-predicate.js'; 4 | import modifiers, {type Modifiers} from './modifiers.js'; 5 | import predicates, {type Predicates} from './predicates.js'; 6 | import test from './test.js'; 7 | 8 | /** 9 | @hidden 10 | */ 11 | export type Main = (value: T, label: string | Function, predicate: BasePredicate, idLabel?: boolean) => void; 12 | 13 | /** 14 | Retrieve the type from the given predicate. 15 | 16 | @example 17 | ``` 18 | import ow, {Infer} from 'ow'; 19 | 20 | const userPredicate = ow.object.exactShape({ 21 | name: ow.string 22 | }); 23 | 24 | type User = Infer; 25 | ``` 26 | */ 27 | export type Infer

= P extends BasePredicate ? T : never; 28 | 29 | // Extends is only necessary for the generated documentation to be cleaner. The loaders below infer the correct type. 30 | export type Ow = { 31 | /** 32 | Test if the value matches the predicate. Throws an `ArgumentError` if the test fails. 33 | 34 | @param value - Value to test. 35 | @param predicate - Predicate to test against. 36 | */ 37 | (value: unknown, predicate: BasePredicate): asserts value is T; 38 | 39 | /** 40 | Test if `value` matches the provided `predicate`. Throws an `ArgumentError` with the specified `label` if the test fails. 41 | 42 | @param value - Value to test. 43 | @param label - Label which should be used in error messages. 44 | @param predicate - Predicate to test against. 45 | */ 46 | (value: unknown, label: string, predicate: BasePredicate): asserts value is T; 47 | 48 | /** 49 | Returns `true` if the value matches the predicate, otherwise returns `false`. 50 | 51 | @param value - Value to test. 52 | @param predicate - Predicate to test against. 53 | */ 54 | isValid: (value: unknown, predicate: BasePredicate) => value is T; 55 | 56 | /** 57 | Create a reusable validator. 58 | 59 | @param predicate - Predicate used in the validator function. 60 | */ 61 | create: ((predicate: BasePredicate) => ReusableValidator) & ((label: string, predicate: BasePredicate) => ReusableValidator); 62 | } & Modifiers & Predicates; 63 | 64 | /** 65 | A reusable validator. 66 | */ 67 | export type ReusableValidator = { 68 | /** 69 | Test if the value matches the predicate. Throws an `ArgumentError` if the test fails. 70 | 71 | @param value - Value to test. 72 | @param label - Override the label which should be used in error messages. 73 | */ 74 | // eslint-disable-next-line @typescript-eslint/prefer-function-type 75 | (value: unknown | T, label?: string): void; 76 | }; 77 | 78 | /** 79 | Turn a `ReusableValidator` into one with a type assertion. 80 | 81 | @example 82 | ``` 83 | const checkUsername = ow.create(ow.string.minLength(3)); 84 | const checkUsername_: AssertingValidator = checkUsername; 85 | ``` 86 | 87 | @example 88 | ``` 89 | const predicate = ow.string.minLength(3); 90 | const checkUsername: AssertingValidator = ow.create(predicate); 91 | ``` 92 | */ 93 | export type AssertingValidator = 94 | T extends ReusableValidator 95 | ? (value: unknown, label?: string) => asserts value is R 96 | : T extends BasePredicate 97 | ? (value: unknown, label?: string) => asserts value is R 98 | : never; 99 | 100 | const ow = (value: unknown, labelOrPredicate: unknown, predicate?: BasePredicate): void => { 101 | if (!isPredicate(labelOrPredicate) && typeof labelOrPredicate !== 'string') { 102 | throw new TypeError(`Expected second argument to be a predicate or a string, got \`${typeof labelOrPredicate}\``); 103 | } 104 | 105 | if (isPredicate(labelOrPredicate)) { 106 | // If the second argument is a predicate, infer the label 107 | const stackFrames = callsites(); 108 | 109 | test(value, () => inferLabel(stackFrames), labelOrPredicate); 110 | 111 | return; 112 | } 113 | 114 | test(value, labelOrPredicate, predicate!); 115 | }; 116 | 117 | Object.defineProperties(ow, { 118 | isValid: { 119 | value(value: unknown, predicate: BasePredicate): boolean { 120 | try { 121 | test(value, '', predicate); 122 | return true; 123 | } catch { 124 | return false; 125 | } 126 | }, 127 | }, 128 | create: { 129 | value: (labelOrPredicate: BasePredicate | string | undefined, predicate?: BasePredicate) => (value: unknown, label?: string): asserts value is T => { 130 | if (isPredicate(labelOrPredicate)) { 131 | const stackFrames = callsites(); 132 | 133 | test(value, label ?? ((): string | void => inferLabel(stackFrames)), labelOrPredicate); 134 | 135 | return; 136 | } 137 | 138 | test(value, label ?? (labelOrPredicate!), predicate!); 139 | }, 140 | }, 141 | }); 142 | 143 | // Can't use `export default predicates(modifiers(ow)) as Ow` because the variable needs a type annotation to avoid a compiler error when used: 144 | // Assertions require every name in the call target to be declared with an explicit type annotation.ts(2775) 145 | // See https://github.com/microsoft/TypeScript/issues/36931 for more details. 146 | const _ow: Ow = predicates(modifiers(ow)) as Ow; 147 | 148 | export default _ow; 149 | 150 | export * from './predicates.js'; 151 | export {ArgumentError} from './argument-error.js'; 152 | 153 | export {Predicate} from './predicates/predicate.js'; 154 | export type {BasePredicate} from './predicates/base-predicate.js'; 155 | -------------------------------------------------------------------------------- /source/modifiers.ts: -------------------------------------------------------------------------------- 1 | import predicates, {type Predicates} from './predicates.js'; 2 | import type {BasePredicate} from './index.js'; 3 | 4 | type Optionalify

= P extends BasePredicate 5 | ? P & BasePredicate 6 | : P; 7 | 8 | export type Modifiers = { 9 | /** 10 | Make the following predicate optional so it doesn't fail when the value is `undefined`. 11 | */ 12 | readonly optional: { 13 | [K in keyof Predicates]: Optionalify 14 | }; 15 | }; 16 | 17 | const modifiers = (object: T): T & Modifiers => { 18 | Object.defineProperties(object, { 19 | optional: { 20 | get: (): Predicates => predicates({}, {optional: true}), 21 | }, 22 | }); 23 | 24 | return object as T & Modifiers; 25 | }; 26 | 27 | export default modifiers; 28 | -------------------------------------------------------------------------------- /source/operators/not.ts: -------------------------------------------------------------------------------- 1 | import randomId from '../utils/random-id.js'; 2 | import {validatorSymbol, type Predicate, type Validator} from '../predicates/predicate.js'; 3 | 4 | /** 5 | Operator which inverts the following validation. 6 | 7 | @hidden 8 | 9 | @param predictate - Predicate to wrap inside the operator. 10 | */ 11 | export const not = (predicate: Predicate): Predicate => { 12 | const originalAddValidator = predicate.addValidator; 13 | 14 | predicate.addValidator = (validator: Validator): Predicate => { 15 | const {validator: function_, message, negatedMessage} = validator; 16 | const placeholder = randomId(); 17 | 18 | validator.message = (value: unknown, label: string): string => ( 19 | negatedMessage 20 | ? negatedMessage(value, label) 21 | : message(value, placeholder).replace(/ to /, '$¬ ').replace(placeholder, label) 22 | ); 23 | 24 | validator.validator = (value: unknown): unknown => !function_(value); 25 | 26 | predicate[validatorSymbol].push(validator); 27 | 28 | predicate.addValidator = originalAddValidator; 29 | 30 | return predicate; 31 | }; 32 | 33 | return predicate; 34 | }; 35 | -------------------------------------------------------------------------------- /source/predicates.ts: -------------------------------------------------------------------------------- 1 | import type {Buffer} from 'node:buffer'; 2 | import type {TypedArray} from './typed-array.js'; 3 | import {StringPredicate} from './predicates/string.js'; 4 | import {NumberPredicate} from './predicates/number.js'; 5 | import {BigIntPredicate} from './predicates/bigint.js'; 6 | import {BooleanPredicate} from './predicates/boolean.js'; 7 | import {Predicate, type PredicateOptions} from './predicates/predicate.js'; 8 | import {ArrayPredicate} from './predicates/array.js'; 9 | import {ObjectPredicate} from './predicates/object.js'; 10 | import {DatePredicate} from './predicates/date.js'; 11 | import {ErrorPredicate} from './predicates/error.js'; 12 | import {MapPredicate} from './predicates/map.js'; 13 | import {WeakMapPredicate} from './predicates/weak-map.js'; 14 | import {SetPredicate} from './predicates/set.js'; 15 | import {WeakSetPredicate} from './predicates/weak-set.js'; 16 | import {TypedArrayPredicate} from './predicates/typed-array.js'; 17 | import {ArrayBufferPredicate} from './predicates/array-buffer.js'; 18 | import {DataViewPredicate} from './predicates/data-view.js'; 19 | import type {BasePredicate} from './predicates/base-predicate.js'; 20 | import {AnyPredicate} from './predicates/any.js'; 21 | 22 | export type Predicates = { 23 | /** 24 | Test the value to be a string. 25 | */ 26 | readonly string: StringPredicate; 27 | 28 | /** 29 | Test the value to be a number. 30 | */ 31 | readonly number: NumberPredicate; 32 | 33 | /** 34 | Test the value to be an bigint. 35 | */ 36 | readonly bigint: BigIntPredicate; 37 | 38 | /** 39 | Test the value to be a boolean. 40 | */ 41 | readonly boolean: BooleanPredicate; 42 | 43 | /** 44 | Test the value to be undefined. 45 | */ 46 | readonly undefined: Predicate; 47 | 48 | /** 49 | Test the value to be null. 50 | */ 51 | readonly null: Predicate; 52 | 53 | /** 54 | Test the value to be null or undefined. 55 | */ 56 | readonly nullOrUndefined: Predicate; 57 | 58 | /** 59 | Test the value to be not a number. 60 | */ 61 | readonly nan: Predicate; 62 | 63 | /** 64 | Test the value to be a Symbol. 65 | */ 66 | readonly symbol: Predicate; 67 | 68 | /** 69 | Test the value to be an array. 70 | */ 71 | readonly array: ArrayPredicate; 72 | 73 | /** 74 | Test the value to be an object. 75 | */ 76 | readonly object: ObjectPredicate; 77 | 78 | /** 79 | Test the value to be a Date. 80 | */ 81 | readonly date: DatePredicate; 82 | 83 | /** 84 | Test the value to be an Error. 85 | */ 86 | readonly error: ErrorPredicate; 87 | 88 | /** 89 | Test the value to be a Map. 90 | */ 91 | readonly map: MapPredicate; 92 | 93 | /** 94 | Test the value to be a WeakMap. 95 | */ 96 | readonly weakMap: WeakMapPredicate; 97 | 98 | /** 99 | Test the value to be a Set. 100 | */ 101 | readonly set: SetPredicate; 102 | 103 | /** 104 | Test the value to be a WeakSet. 105 | */ 106 | readonly weakSet: WeakSetPredicate; 107 | 108 | /** 109 | Test the value to be a Function. 110 | */ 111 | readonly function: Predicate; 112 | 113 | /** 114 | Test the value to be a Buffer. 115 | */ 116 | readonly buffer: Predicate; 117 | 118 | /** 119 | Test the value to be a RegExp. 120 | */ 121 | readonly regExp: Predicate; 122 | 123 | /** 124 | Test the value to be a Promise. 125 | */ 126 | readonly promise: Predicate>; 127 | 128 | /** 129 | Test the value to be a typed array. 130 | */ 131 | readonly typedArray: TypedArrayPredicate; 132 | 133 | /** 134 | Test the value to be a Int8Array. 135 | */ 136 | readonly int8Array: TypedArrayPredicate; 137 | 138 | /** 139 | Test the value to be a Uint8Array. 140 | */ 141 | readonly uint8Array: TypedArrayPredicate; 142 | 143 | /** 144 | Test the value to be a Uint8ClampedArray. 145 | */ 146 | readonly uint8ClampedArray: TypedArrayPredicate; 147 | 148 | /** 149 | Test the value to be a Int16Array. 150 | */ 151 | readonly int16Array: TypedArrayPredicate; 152 | 153 | /** 154 | Test the value to be a Uint16Array. 155 | */ 156 | readonly uint16Array: TypedArrayPredicate; 157 | 158 | /** 159 | Test the value to be a Int32Array. 160 | */ 161 | readonly int32Array: TypedArrayPredicate; 162 | 163 | /** 164 | Test the value to be a Uint32Array. 165 | */ 166 | readonly uint32Array: TypedArrayPredicate; 167 | 168 | /** 169 | Test the value to be a Float32Array. 170 | */ 171 | readonly float32Array: TypedArrayPredicate; 172 | 173 | /** 174 | Test the value to be a Float64Array. 175 | */ 176 | readonly float64Array: TypedArrayPredicate; 177 | 178 | /** 179 | Test the value to be a ArrayBuffer. 180 | */ 181 | readonly arrayBuffer: ArrayBufferPredicate; 182 | 183 | /** 184 | Test the value to be a SharedArrayBuffer. 185 | */ 186 | readonly sharedArrayBuffer: ArrayBufferPredicate; 187 | 188 | /** 189 | Test the value to be a DataView. 190 | */ 191 | readonly dataView: DataViewPredicate; 192 | 193 | /** 194 | Test the value to be Iterable. 195 | */ 196 | readonly iterable: Predicate>; 197 | 198 | /** 199 | Test that the value matches at least one of the given predicates. 200 | */ 201 | any: ((p1: BasePredicate) => AnyPredicate) 202 | & ((p1: BasePredicate, p2: BasePredicate) => AnyPredicate) 203 | & ((p1: BasePredicate, p2: BasePredicate, p3: BasePredicate) => AnyPredicate) 204 | & ((p1: BasePredicate, p2: BasePredicate, p3: BasePredicate, p4: BasePredicate) => AnyPredicate) 205 | & ((p1: BasePredicate, p2: BasePredicate, p3: BasePredicate, p4: BasePredicate, p5: BasePredicate) => AnyPredicate) 206 | & ((p1: BasePredicate, p2: BasePredicate, p3: BasePredicate, p4: BasePredicate, p5: BasePredicate, p6: BasePredicate) => AnyPredicate) 207 | & ((p1: BasePredicate, p2: BasePredicate, p3: BasePredicate, p4: BasePredicate, p5: BasePredicate, p6: BasePredicate, p7: BasePredicate) => AnyPredicate) 208 | & ((p1: BasePredicate, p2: BasePredicate, p3: BasePredicate, p4: BasePredicate, p5: BasePredicate, p6: BasePredicate, p7: BasePredicate, p8: BasePredicate) => AnyPredicate) 209 | & ((p1: BasePredicate, p2: BasePredicate, p3: BasePredicate, p4: BasePredicate, p5: BasePredicate, p6: BasePredicate, p7: BasePredicate, p8: BasePredicate, p9: BasePredicate) => AnyPredicate) 210 | & ((p1: BasePredicate, p2: BasePredicate, p3: BasePredicate, p4: BasePredicate, p5: BasePredicate, p6: BasePredicate, p7: BasePredicate, p8: BasePredicate, p9: BasePredicate, p10: BasePredicate) => AnyPredicate) 211 | & ((...predicate: BasePredicate[]) => AnyPredicate); 212 | }; 213 | 214 | const predicates = (object: T, options?: PredicateOptions): T & Predicates => { 215 | Object.defineProperties(object, { 216 | string: { 217 | get: (): StringPredicate => new StringPredicate(options), 218 | }, 219 | number: { 220 | get: (): NumberPredicate => new NumberPredicate(options), 221 | }, 222 | bigint: { 223 | get: (): BigIntPredicate => new BigIntPredicate(options), 224 | }, 225 | boolean: { 226 | get: (): BooleanPredicate => new BooleanPredicate(options), 227 | }, 228 | undefined: { 229 | get: (): Predicate => new Predicate('undefined', options), 230 | }, 231 | null: { 232 | get: (): Predicate => new Predicate('null', options), 233 | }, 234 | nullOrUndefined: { 235 | get: (): Predicate => new Predicate('nullOrUndefined', options), 236 | }, 237 | nan: { 238 | get: (): Predicate => new Predicate('nan', options), 239 | }, 240 | symbol: { 241 | get: (): Predicate => new Predicate('symbol', options), 242 | }, 243 | array: { 244 | get: (): ArrayPredicate => new ArrayPredicate(options), 245 | }, 246 | object: { 247 | get: (): ObjectPredicate => new ObjectPredicate(options), 248 | }, 249 | date: { 250 | get: (): DatePredicate => new DatePredicate(options), 251 | }, 252 | error: { 253 | get: (): ErrorPredicate => new ErrorPredicate(options), 254 | }, 255 | map: { 256 | get: (): MapPredicate => new MapPredicate(options), 257 | }, 258 | weakMap: { 259 | get: (): WeakMapPredicate => new WeakMapPredicate(options), 260 | }, 261 | set: { 262 | get: (): SetPredicate => new SetPredicate(options), 263 | }, 264 | weakSet: { 265 | get: (): WeakSetPredicate => new WeakSetPredicate(options), 266 | }, 267 | function: { 268 | get: (): Predicate => new Predicate('Function', options), 269 | }, 270 | buffer: { 271 | get: (): Predicate => new Predicate('Buffer', options), 272 | }, 273 | regExp: { 274 | get: (): Predicate => new Predicate('RegExp', options), 275 | }, 276 | promise: { 277 | get: (): Predicate => new Predicate('Promise', options), 278 | }, 279 | typedArray: { 280 | get: (): TypedArrayPredicate => new TypedArrayPredicate('TypedArray', options), 281 | }, 282 | int8Array: { 283 | get: (): TypedArrayPredicate => new TypedArrayPredicate('Int8Array', options), 284 | }, 285 | uint8Array: { 286 | get: (): TypedArrayPredicate => new TypedArrayPredicate('Uint8Array', options), 287 | }, 288 | uint8ClampedArray: { 289 | get: (): TypedArrayPredicate => new TypedArrayPredicate('Uint8ClampedArray', options), 290 | }, 291 | int16Array: { 292 | get: (): TypedArrayPredicate => new TypedArrayPredicate('Int16Array', options), 293 | }, 294 | uint16Array: { 295 | get: (): TypedArrayPredicate => new TypedArrayPredicate('Uint16Array', options), 296 | }, 297 | int32Array: { 298 | get: (): TypedArrayPredicate => new TypedArrayPredicate('Int32Array', options), 299 | }, 300 | uint32Array: { 301 | get: (): TypedArrayPredicate => new TypedArrayPredicate('Uint32Array', options), 302 | }, 303 | float32Array: { 304 | get: (): TypedArrayPredicate => new TypedArrayPredicate('Float32Array', options), 305 | }, 306 | float64Array: { 307 | get: (): TypedArrayPredicate => new TypedArrayPredicate('Float64Array', options), 308 | }, 309 | arrayBuffer: { 310 | get: (): ArrayBufferPredicate => new ArrayBufferPredicate('ArrayBuffer', options), 311 | }, 312 | sharedArrayBuffer: { 313 | get: (): ArrayBufferPredicate => new ArrayBufferPredicate('SharedArrayBuffer', options), 314 | }, 315 | dataView: { 316 | get: (): DataViewPredicate => new DataViewPredicate(options), 317 | }, 318 | iterable: { 319 | get: (): Predicate => new Predicate('Iterable', options), 320 | }, 321 | any: { 322 | value: (...predicates: BasePredicate[]): AnyPredicate => new AnyPredicate(predicates, options), 323 | }, 324 | }); 325 | 326 | return object as T & Predicates; 327 | }; 328 | 329 | export default predicates; 330 | 331 | export {ObjectPredicate} from './predicates/object.js'; 332 | export type {Shape} from './predicates/object.js'; 333 | export {StringPredicate} from './predicates/string.js'; 334 | export {NumberPredicate} from './predicates/number.js'; 335 | export {BigIntPredicate} from './predicates/bigint.js'; 336 | export {BooleanPredicate} from './predicates/boolean.js'; 337 | export {ArrayPredicate} from './predicates/array.js'; 338 | export {DatePredicate} from './predicates/date.js'; 339 | export {ErrorPredicate} from './predicates/error.js'; 340 | export {MapPredicate} from './predicates/map.js'; 341 | export {WeakMapPredicate} from './predicates/weak-map.js'; 342 | export {SetPredicate} from './predicates/set.js'; 343 | export {WeakSetPredicate} from './predicates/weak-set.js'; 344 | export {TypedArrayPredicate} from './predicates/typed-array.js'; 345 | export {ArrayBufferPredicate} from './predicates/array-buffer.js'; 346 | export {DataViewPredicate} from './predicates/data-view.js'; 347 | export {AnyPredicate} from './predicates/any.js'; 348 | -------------------------------------------------------------------------------- /source/predicates/any.ts: -------------------------------------------------------------------------------- 1 | import {ArgumentError} from '../argument-error.js'; 2 | import type {Main} from '../index.js'; 3 | import {generateArgumentErrorMessage} from '../utils/generate-argument-error-message.js'; 4 | import {testSymbol, type BasePredicate} from './base-predicate.js'; 5 | import type {PredicateOptions} from './predicate.js'; 6 | 7 | /** 8 | @hidden 9 | */ 10 | export class AnyPredicate implements BasePredicate { 11 | constructor( 12 | private readonly predicates: BasePredicate[], 13 | private readonly options: PredicateOptions = {}, 14 | ) {} 15 | 16 | [testSymbol](value: T, main: Main, label: string | Function, idLabel: boolean): asserts value { 17 | const errors = new Map>(); 18 | 19 | for (const predicate of this.predicates) { 20 | try { 21 | main(value, label, predicate, idLabel); 22 | return; 23 | } catch (error: unknown) { 24 | if (value === undefined && this.options.optional === true) { 25 | return; 26 | } 27 | 28 | // If we received an ArgumentError, then.. 29 | if (error instanceof ArgumentError) { 30 | // Iterate through every error reported. 31 | for (const [key, value] of error.validationErrors.entries()) { 32 | // Get the current errors set, if any. 33 | const alreadyPresent = errors.get(key); 34 | 35 | // Add all errors under the same key 36 | errors.set(key, new Set([...alreadyPresent ?? [], ...value])); 37 | } 38 | } 39 | } 40 | } 41 | 42 | if (errors.size > 0) { 43 | // Generate the `error.message` property. 44 | const message = generateArgumentErrorMessage(errors, true); 45 | 46 | throw new ArgumentError( 47 | `Any predicate failed with the following errors:\n${message}`, 48 | main, 49 | errors, 50 | ); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /source/predicates/array-buffer.ts: -------------------------------------------------------------------------------- 1 | import {Predicate} from './predicate.js'; 2 | 3 | export class ArrayBufferPredicate extends Predicate { 4 | /** 5 | Test an array buffer to have a specific byte length. 6 | 7 | @param byteLength - The byte length of the array buffer. 8 | */ 9 | byteLength(byteLength: number): this { 10 | return this.addValidator({ 11 | message: (value, label) => `Expected ${label} to have byte length of \`${byteLength}\`, got \`${value.byteLength}\``, 12 | validator: value => value.byteLength === byteLength, 13 | }); 14 | } 15 | 16 | /** 17 | Test an array buffer to have a minimum byte length. 18 | 19 | @param byteLength - The minimum byte length of the array buffer. 20 | */ 21 | minByteLength(byteLength: number): this { 22 | return this.addValidator({ 23 | message: (value, label) => `Expected ${label} to have a minimum byte length of \`${byteLength}\`, got \`${value.byteLength}\``, 24 | validator: value => value.byteLength >= byteLength, 25 | negatedMessage: (value, label) => `Expected ${label} to have a maximum byte length of \`${byteLength - 1}\`, got \`${value.byteLength}\``, 26 | }); 27 | } 28 | 29 | /** 30 | Test an array buffer to have a minimum byte length. 31 | 32 | @param length - The minimum byte length of the array buffer. 33 | */ 34 | maxByteLength(byteLength: number): this { 35 | return this.addValidator({ 36 | message: (value, label) => `Expected ${label} to have a maximum byte length of \`${byteLength}\`, got \`${value.byteLength}\``, 37 | validator: value => value.byteLength <= byteLength, 38 | negatedMessage: (value, label) => `Expected ${label} to have a minimum byte length of \`${byteLength + 1}\`, got \`${value.byteLength}\``, 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /source/predicates/array.ts: -------------------------------------------------------------------------------- 1 | import {deepEqual} from 'fast-equals'; 2 | import {exact} from '../utils/match-shape.js'; 3 | import ofType from '../utils/of-type.js'; 4 | import type {BasePredicate} from './base-predicate.js'; 5 | import {Predicate, type PredicateOptions} from './predicate.js'; 6 | import type {Shape} from './object.js'; 7 | 8 | export class ArrayPredicate extends Predicate { 9 | /** 10 | @hidden 11 | */ 12 | constructor(options?: PredicateOptions) { 13 | super('array', options); 14 | } 15 | 16 | /** 17 | Test an array to have a specific length. 18 | 19 | @param length - The length of the array. 20 | */ 21 | length(length: number): this { 22 | return this.addValidator({ 23 | message: (value, label) => `Expected ${label} to have length \`${length}\`, got \`${value.length}\``, 24 | validator: value => value.length === length, 25 | }); 26 | } 27 | 28 | /** 29 | Test an array to have a minimum length. 30 | 31 | @param length - The minimum length of the array. 32 | */ 33 | minLength(length: number): this { 34 | return this.addValidator({ 35 | message: (value, label) => `Expected ${label} to have a minimum length of \`${length}\`, got \`${value.length}\``, 36 | validator: value => value.length >= length, 37 | negatedMessage: (value, label) => `Expected ${label} to have a maximum length of \`${length - 1}\`, got \`${value.length}\``, 38 | }); 39 | } 40 | 41 | /** 42 | Test an array to have a maximum length. 43 | 44 | @param length - The maximum length of the array. 45 | */ 46 | maxLength(length: number): this { 47 | return this.addValidator({ 48 | message: (value, label) => `Expected ${label} to have a maximum length of \`${length}\`, got \`${value.length}\``, 49 | validator: value => value.length <= length, 50 | negatedMessage: (value, label) => `Expected ${label} to have a minimum length of \`${length + 1}\`, got \`${value.length}\``, 51 | }); 52 | } 53 | 54 | /** 55 | Test an array to start with a specific value. The value is tested by identity, not structure. 56 | 57 | @param searchElement - The value that should be the start of the array. 58 | */ 59 | startsWith(searchElement: T): this { 60 | return this.addValidator({ 61 | message: (value, label) => `Expected ${label} to start with \`${searchElement}\`, got \`${value[0]}\``, 62 | validator: value => value[0] === searchElement, 63 | }); 64 | } 65 | 66 | /** 67 | Test an array to end with a specific value. The value is tested by identity, not structure. 68 | 69 | @param searchElement - The value that should be the end of the array. 70 | */ 71 | endsWith(searchElement: T): this { 72 | return this.addValidator({ 73 | message: (value, label) => `Expected ${label} to end with \`${searchElement}\`, got \`${value.at(-1)}\``, 74 | validator: value => value.at(-1) === searchElement, 75 | }); 76 | } 77 | 78 | /** 79 | Test an array to include all the provided elements. The values are tested by identity, not structure. 80 | 81 | @param searchElements - The values that should be included in the array. 82 | */ 83 | includes(...searchElements: readonly T[]): this { 84 | return this.addValidator({ 85 | message: (value, label) => `Expected ${label} to include all elements of \`${JSON.stringify(searchElements)}\`, got \`${JSON.stringify(value)}\``, 86 | validator: value => searchElements.every(element => value.includes(element)), 87 | }); 88 | } 89 | 90 | /** 91 | Test an array to include any of the provided elements. The values are tested by identity, not structure. 92 | 93 | @param searchElements - The values that should be included in the array. 94 | */ 95 | includesAny(...searchElements: readonly T[]): this { 96 | return this.addValidator({ 97 | message: (value, label) => `Expected ${label} to include any element of \`${JSON.stringify(searchElements)}\`, got \`${JSON.stringify(value)}\``, 98 | validator: value => searchElements.some(element => value.includes(element)), 99 | }); 100 | } 101 | 102 | /** 103 | Test an array to be empty. 104 | */ 105 | get empty(): this { 106 | return this.addValidator({ 107 | message: (value, label) => `Expected ${label} to be empty, got \`${JSON.stringify(value)}\``, 108 | validator: value => value.length === 0, 109 | }); 110 | } 111 | 112 | /** 113 | Test an array to be not empty. 114 | */ 115 | get nonEmpty(): this { 116 | return this.addValidator({ 117 | message: (_, label) => `Expected ${label} to not be empty`, 118 | validator: value => value.length > 0, 119 | }); 120 | } 121 | 122 | /** 123 | Test an array to be deeply equal to the provided array. 124 | 125 | @param expected - Expected value to match. 126 | */ 127 | deepEqual(expected: readonly T[]): this { 128 | return this.addValidator({ 129 | message: (value, label) => `Expected ${label} to be deeply equal to \`${JSON.stringify(expected)}\`, got \`${JSON.stringify(value)}\``, 130 | validator: value => deepEqual(value, expected), 131 | }); 132 | } 133 | 134 | /** 135 | Test all elements in the array to match to provided predicate. 136 | 137 | @param predicate - The predicate that should be applied against every individual item. 138 | 139 | @example 140 | ``` 141 | ow(['a', 1], ow.array.ofType(ow.any(ow.string, ow.number))); 142 | ``` 143 | */ 144 | ofType(predicate: BasePredicate): ArrayPredicate { 145 | // TODO [typescript@>=6] If higher-kinded types are supported natively by typescript, refactor `addValidator` to use them to avoid the usage of `any`. Otherwise, bump or remove this TODO. 146 | return this.addValidator({ 147 | message: (_, label, error) => `(${label}) ${error}`, 148 | validator: value => ofType(value, 'values', predicate), 149 | }) as ArrayPredicate; 150 | } 151 | 152 | /** 153 | Test if the elements in the array exactly matches the elements placed at the same indices in the predicates array. 154 | 155 | @param predicates - Predicates to test the array against. Describes what the tested array should look like. 156 | 157 | @example 158 | ``` 159 | ow(['1', 2], ow.array.exactShape([ow.string, ow.number])); 160 | ``` 161 | */ 162 | exactShape(predicates: Predicate[]): this { 163 | const shape = predicates as unknown as Shape; 164 | 165 | return this.addValidator({ 166 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 167 | message: (_, label, message) => `${message.replace('Expected', 'Expected element')} in ${label}`, 168 | validator: object => exact(object, shape, undefined, true), 169 | }); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /source/predicates/base-predicate.ts: -------------------------------------------------------------------------------- 1 | import type {Main} from '../index.js'; 2 | 3 | /** 4 | @hidden 5 | */ 6 | export const testSymbol: unique symbol = Symbol('test'); 7 | 8 | /** 9 | @hidden 10 | */ 11 | export const isPredicate = (value: unknown): value is BasePredicate => Boolean((value as any)?.[testSymbol]); 12 | 13 | /** 14 | @hidden 15 | */ 16 | export type BasePredicate = { 17 | [testSymbol](value: T, main: Main, label: string | Function, idLabel?: boolean): void; 18 | }; 19 | -------------------------------------------------------------------------------- /source/predicates/bigint.ts: -------------------------------------------------------------------------------- 1 | import {Predicate, type PredicateOptions} from './predicate.js'; 2 | 3 | export class BigIntPredicate extends Predicate { 4 | /** 5 | @hidden 6 | */ 7 | constructor(options?: PredicateOptions) { 8 | super('bigint', options); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /source/predicates/boolean.ts: -------------------------------------------------------------------------------- 1 | import {Predicate, type PredicateOptions} from './predicate.js'; 2 | 3 | export class BooleanPredicate extends Predicate { 4 | /** 5 | @hidden 6 | */ 7 | constructor(options?: PredicateOptions) { 8 | super('boolean', options); 9 | } 10 | 11 | /** 12 | Test a boolean to be true. 13 | */ 14 | get true(): this { 15 | return this.addValidator({ 16 | message: (value, label) => `Expected ${label} to be true, got ${value}`, 17 | validator: value => value, 18 | }); 19 | } 20 | 21 | /** 22 | Test a boolean to be false. 23 | */ 24 | get false(): this { 25 | return this.addValidator({ 26 | message: (value, label) => `Expected ${label} to be false, got ${value}`, 27 | validator: value => !value, 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /source/predicates/data-view.ts: -------------------------------------------------------------------------------- 1 | import {Predicate, type PredicateOptions} from './predicate.js'; 2 | 3 | export class DataViewPredicate extends Predicate { 4 | /** 5 | @hidden 6 | */ 7 | constructor(options?: PredicateOptions) { 8 | super('DataView', options); 9 | } 10 | 11 | /** 12 | Test a DataView to have a specific byte length. 13 | 14 | @param byteLength - The byte length of the DataView. 15 | */ 16 | byteLength(byteLength: number): this { 17 | return this.addValidator({ 18 | message: (value, label) => `Expected ${label} to have byte length of \`${byteLength}\`, got \`${value.byteLength}\``, 19 | validator: value => value.byteLength === byteLength, 20 | }); 21 | } 22 | 23 | /** 24 | Test a DataView to have a minimum byte length. 25 | 26 | @param byteLength - The minimum byte length of the DataView. 27 | */ 28 | minByteLength(byteLength: number): this { 29 | return this.addValidator({ 30 | message: (value, label) => `Expected ${label} to have a minimum byte length of \`${byteLength}\`, got \`${value.byteLength}\``, 31 | validator: value => value.byteLength >= byteLength, 32 | negatedMessage: (value, label) => `Expected ${label} to have a maximum byte length of \`${byteLength - 1}\`, got \`${value.byteLength}\``, 33 | }); 34 | } 35 | 36 | /** 37 | Test a DataView to have a minimum byte length. 38 | 39 | @param length - The minimum byte length of the DataView. 40 | */ 41 | maxByteLength(byteLength: number): this { 42 | return this.addValidator({ 43 | message: (value, label) => `Expected ${label} to have a maximum byte length of \`${byteLength}\`, got \`${value.byteLength}\``, 44 | validator: value => value.byteLength <= byteLength, 45 | negatedMessage: (value, label) => `Expected ${label} to have a minimum byte length of \`${byteLength + 1}\`, got \`${value.byteLength}\``, 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /source/predicates/date.ts: -------------------------------------------------------------------------------- 1 | import {Predicate, type PredicateOptions} from './predicate.js'; 2 | 3 | export class DatePredicate extends Predicate { 4 | /** 5 | @hidden 6 | */ 7 | constructor(options?: PredicateOptions) { 8 | super('date', options); 9 | } 10 | 11 | /** 12 | Test a date to be before another date. 13 | 14 | @param date - Maximum value. 15 | */ 16 | before(date: Date): this { 17 | return this.addValidator({ 18 | message: (value, label) => `Expected ${label} ${value.toISOString()} to be before ${date.toISOString()}`, 19 | validator: value => value.getTime() < date.getTime(), 20 | }); 21 | } 22 | 23 | /** 24 | Test a date to be before another date. 25 | 26 | @param date - Minimum value. 27 | */ 28 | after(date: Date): this { 29 | return this.addValidator({ 30 | message: (value, label) => `Expected ${label} ${value.toISOString()} to be after ${date.toISOString()}`, 31 | validator: value => value.getTime() > date.getTime(), 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /source/predicates/error.ts: -------------------------------------------------------------------------------- 1 | import {Predicate, type PredicateOptions} from './predicate.js'; 2 | 3 | export class ErrorPredicate extends Predicate { 4 | /** 5 | @hidden 6 | */ 7 | constructor(options?: PredicateOptions) { 8 | super('error', options); 9 | } 10 | 11 | /** 12 | Test an error to have a specific name. 13 | 14 | @param expected - Expected name of the Error. 15 | */ 16 | name(expected: string): this { 17 | return this.addValidator({ 18 | message: (error, label) => `Expected ${label} to have name \`${expected}\`, got \`${error.name}\``, 19 | validator: error => error.name === expected, 20 | }); 21 | } 22 | 23 | /** 24 | Test an error to have a specific message. 25 | 26 | @param expected - Expected message of the Error. 27 | */ 28 | override message(expected: string): this { 29 | return this.addValidator({ 30 | message: (error, label) => `Expected ${label} message to be \`${expected}\`, got \`${error.message}\``, 31 | validator: error => error.message === expected, 32 | }); 33 | } 34 | 35 | /** 36 | Test the error message to include a specific message. 37 | 38 | @param message - Message that should be included in the error. 39 | */ 40 | messageIncludes(message: string): this { 41 | return this.addValidator({ 42 | message: (error, label) => `Expected ${label} message to include \`${message}\`, got \`${error.message}\``, 43 | validator: error => error.message.includes(message), 44 | }); 45 | } 46 | 47 | /** 48 | Test the error object to have specific keys. 49 | 50 | @param keys - One or more keys which should be part of the error object. 51 | */ 52 | hasKeys(...keys: readonly string[]): this { 53 | return this.addValidator({ 54 | message: (_, label) => `Expected ${label} message to have keys \`${keys.join('`, `')}\``, 55 | validator: error => keys.every(key => Object.hasOwn(error, key)), 56 | }); 57 | } 58 | 59 | /** 60 | Test an error to be of a specific instance type. 61 | 62 | @param instance - The expected instance type of the error. 63 | */ 64 | instanceOf(instance: Function): this { 65 | return this.addValidator({ 66 | message: (error, label) => `Expected ${label} \`${error.name}\` to be of type \`${instance.name}\``, 67 | validator: error => error instanceof instance, 68 | }); 69 | } 70 | 71 | /** 72 | Test an Error to be a TypeError. 73 | */ 74 | get typeError(): this { 75 | return this.instanceOf(TypeError); 76 | } 77 | 78 | /** 79 | Test an Error to be an EvalError. 80 | */ 81 | get evalError(): this { 82 | return this.instanceOf(EvalError); 83 | } 84 | 85 | /** 86 | Test an Error to be a RangeError. 87 | */ 88 | get rangeError(): this { 89 | return this.instanceOf(RangeError); 90 | } 91 | 92 | /** 93 | Test an Error to be a ReferenceError. 94 | */ 95 | get referenceError(): this { 96 | return this.instanceOf(ReferenceError); 97 | } 98 | 99 | /** 100 | Test an Error to be a SyntaxError. 101 | */ 102 | get syntaxError(): this { 103 | return this.instanceOf(SyntaxError); 104 | } 105 | 106 | /** 107 | Test an Error to be a URIError. 108 | */ 109 | get uriError(): this { 110 | return this.instanceOf(URIError); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /source/predicates/map.ts: -------------------------------------------------------------------------------- 1 | import {deepEqual} from 'fast-equals'; 2 | import hasItems from '../utils/has-items.js'; 3 | import ofType from '../utils/of-type.js'; 4 | import {Predicate, type PredicateOptions} from './predicate.js'; 5 | 6 | export class MapPredicate extends Predicate> { 7 | /** 8 | @hidden 9 | */ 10 | constructor(options?: PredicateOptions) { 11 | super('Map', options); 12 | } 13 | 14 | /** 15 | Test a Map to have a specific size. 16 | 17 | @param size - The size of the Map. 18 | */ 19 | size(size: number): this { 20 | return this.addValidator({ 21 | message: (map, label) => `Expected ${label} to have size \`${size}\`, got \`${map.size}\``, 22 | validator: map => map.size === size, 23 | }); 24 | } 25 | 26 | /** 27 | Test an Map to have a minimum size. 28 | 29 | @param size - The minimum size of the Map. 30 | */ 31 | minSize(size: number): this { 32 | return this.addValidator({ 33 | message: (map, label) => `Expected ${label} to have a minimum size of \`${size}\`, got \`${map.size}\``, 34 | validator: map => map.size >= size, 35 | negatedMessage: (map, label) => `Expected ${label} to have a maximum size of \`${size - 1}\`, got \`${map.size}\``, 36 | }); 37 | } 38 | 39 | /** 40 | Test an Map to have a maximum size. 41 | 42 | @param size - The maximum size of the Map. 43 | */ 44 | maxSize(size: number): this { 45 | return this.addValidator({ 46 | message: (map, label) => `Expected ${label} to have a maximum size of \`${size}\`, got \`${map.size}\``, 47 | validator: map => map.size <= size, 48 | negatedMessage: (map, label) => `Expected ${label} to have a minimum size of \`${size + 1}\`, got \`${map.size}\``, 49 | }); 50 | } 51 | 52 | /** 53 | Test a Map to include all the provided keys. The keys are tested by identity, not structure. 54 | 55 | @param keys - The keys that should be a key in the Map. 56 | */ 57 | hasKeys(...keys: readonly T1[]): this { 58 | return this.addValidator({ 59 | message: (_, label, missingKeys) => `Expected ${label} to have keys \`${JSON.stringify(missingKeys)}\``, 60 | validator: map => hasItems(map, keys), 61 | }); 62 | } 63 | 64 | /** 65 | Test a Map to include any of the provided keys. The keys are tested by identity, not structure. 66 | 67 | @param keys - The keys that could be a key in the Map. 68 | */ 69 | hasAnyKeys(...keys: readonly T1[]): this { 70 | return this.addValidator({ 71 | message: (_, label) => `Expected ${label} to have any key of \`${JSON.stringify(keys)}\``, 72 | validator: map => keys.some(key => map.has(key)), 73 | }); 74 | } 75 | 76 | /** 77 | Test a Map to include all the provided values. The values are tested by identity, not structure. 78 | 79 | @param values - The values that should be a value in the Map. 80 | */ 81 | hasValues(...values: readonly T2[]): this { 82 | return this.addValidator({ 83 | message: (_, label, missingValues) => `Expected ${label} to have values \`${JSON.stringify(missingValues)}\``, 84 | validator: map => hasItems(new Set(map.values()), values), 85 | }); 86 | } 87 | 88 | /** 89 | Test a Map to include any of the provided values. The values are tested by identity, not structure. 90 | 91 | @param values - The values that could be a value in the Map. 92 | */ 93 | hasAnyValues(...values: readonly T2[]): this { 94 | return this.addValidator({ 95 | message: (_, label) => `Expected ${label} to have any value of \`${JSON.stringify(values)}\``, 96 | validator(map) { 97 | const valueSet = new Set(map.values()); 98 | return values.some(key => valueSet.has(key)); 99 | }, 100 | }); 101 | } 102 | 103 | /** 104 | Test all the keys in the Map to match the provided predicate. 105 | 106 | @param predicate - The predicate that should be applied against every key in the Map. 107 | */ 108 | keysOfType(predicate: Predicate): this { 109 | return this.addValidator({ 110 | message: (_, label, error) => `(${label}) ${error}`, 111 | validator: map => ofType(map.keys(), 'keys', predicate), 112 | }); 113 | } 114 | 115 | /** 116 | Test all the values in the Map to match the provided predicate. 117 | 118 | @param predicate - The predicate that should be applied against every value in the Map. 119 | */ 120 | valuesOfType(predicate: Predicate): this { 121 | return this.addValidator({ 122 | message: (_, label, error) => `(${label}) ${error}`, 123 | validator: map => ofType(map.values(), 'values', predicate), 124 | }); 125 | } 126 | 127 | /** 128 | Test a Map to be empty. 129 | */ 130 | get empty(): this { 131 | return this.addValidator({ 132 | message: (map, label) => `Expected ${label} to be empty, got \`${JSON.stringify([...map])}\``, 133 | validator: map => map.size === 0, 134 | }); 135 | } 136 | 137 | /** 138 | Test a Map to be not empty. 139 | */ 140 | get nonEmpty(): this { 141 | return this.addValidator({ 142 | message: (_, label) => `Expected ${label} to not be empty`, 143 | validator: map => map.size > 0, 144 | }); 145 | } 146 | 147 | /** 148 | Test a Map to be deeply equal to the provided Map. 149 | 150 | @param expected - Expected Map to match. 151 | */ 152 | deepEqual(expected: Map): this { 153 | return this.addValidator({ 154 | message: (map, label) => `Expected ${label} to be deeply equal to \`${JSON.stringify([...expected])}\`, got \`${JSON.stringify([...map])}\``, 155 | validator: map => deepEqual(map, expected), 156 | }); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /source/predicates/number.ts: -------------------------------------------------------------------------------- 1 | import is from '@sindresorhus/is'; 2 | import {Predicate, type PredicateOptions} from './predicate.js'; 3 | 4 | export class NumberPredicate extends Predicate { 5 | /** 6 | @hidden 7 | */ 8 | constructor(options?: PredicateOptions) { 9 | super('number', options); 10 | } 11 | 12 | /** 13 | Test a number to be in a specified range. 14 | 15 | @param start - Start of the range. 16 | @param end - End of the range. 17 | */ 18 | inRange(start: number, end: number): this { 19 | return this.addValidator({ 20 | message: (value, label) => `Expected ${label} to be in range [${start}..${end}], got ${value}`, 21 | validator: value => is.inRange(value, [start, end]), 22 | }); 23 | } 24 | 25 | /** 26 | Test a number to be greater than the provided value. 27 | 28 | @param number - Minimum value. 29 | */ 30 | greaterThan(number: number): this { 31 | return this.addValidator({ 32 | message: (value, label) => `Expected ${label} to be greater than ${number}, got ${value}`, 33 | validator: value => value > number, 34 | }); 35 | } 36 | 37 | /** 38 | Test a number to be greater than or equal to the provided value. 39 | 40 | @param number - Minimum value. 41 | */ 42 | greaterThanOrEqual(number: number): this { 43 | return this.addValidator({ 44 | message: (value, label) => `Expected ${label} to be greater than or equal to ${number}, got ${value}`, 45 | validator: value => value >= number, 46 | }); 47 | } 48 | 49 | /** 50 | Test a number to be less than the provided value. 51 | 52 | @param number - Maximum value. 53 | */ 54 | lessThan(number: number): this { 55 | return this.addValidator({ 56 | message: (value, label) => `Expected ${label} to be less than ${number}, got ${value}`, 57 | validator: value => value < number, 58 | }); 59 | } 60 | 61 | /** 62 | Test a number to be less than or equal to the provided value. 63 | 64 | @param number - Minimum value. 65 | */ 66 | lessThanOrEqual(number: number): this { 67 | return this.addValidator({ 68 | message: (value, label) => `Expected ${label} to be less than or equal to ${number}, got ${value}`, 69 | validator: value => value <= number, 70 | }); 71 | } 72 | 73 | /** 74 | Test a number to be equal to a specified number. 75 | 76 | @param expected - Expected value to match. 77 | */ 78 | equal(expected: number): this { 79 | return this.addValidator({ 80 | message: (value, label) => `Expected ${label} to be equal to ${expected}, got ${value}`, 81 | validator: value => value === expected, 82 | }); 83 | } 84 | 85 | /** 86 | Test if a number is an element of the provided list. 87 | 88 | @param list - List of possible values. 89 | */ 90 | oneOf(list: readonly number[]): this { 91 | return this.addValidator({ 92 | message(value, label) { 93 | let printedList = JSON.stringify(list); 94 | 95 | if (list.length > 10) { 96 | const overflow = list.length - 10; 97 | printedList = JSON.stringify(list.slice(0, 10)).replace(/]$/, `,…+${overflow} more]`); 98 | } 99 | 100 | return `Expected ${label} to be one of \`${printedList}\`, got ${value}`; 101 | }, 102 | validator: value => list.includes(value), 103 | }); 104 | } 105 | 106 | /** 107 | Test a number to be an integer. 108 | */ 109 | get integer(): this { 110 | return this.addValidator({ 111 | message: (value, label) => `Expected ${label} to be an integer, got ${value}`, 112 | validator: value => is.integer(value), 113 | }); 114 | } 115 | 116 | /** 117 | Test a number to be finite. 118 | */ 119 | get finite(): this { 120 | return this.addValidator({ 121 | message: (value, label) => `Expected ${label} to be finite, got ${value}`, 122 | validator: value => !is.infinite(value), 123 | }); 124 | } 125 | 126 | /** 127 | Test a number to be infinite. 128 | */ 129 | get infinite(): this { 130 | return this.addValidator({ 131 | message: (value, label) => `Expected ${label} to be infinite, got ${value}`, 132 | validator: value => is.infinite(value), 133 | }); 134 | } 135 | 136 | /** 137 | Test a number to be positive. 138 | */ 139 | get positive(): this { 140 | return this.addValidator({ 141 | message: (value, label) => `Expected ${label} to be positive, got ${value}`, 142 | validator: value => value > 0, 143 | }); 144 | } 145 | 146 | /** 147 | Test a number to be negative. 148 | */ 149 | get negative(): this { 150 | return this.addValidator({ 151 | message: (value, label) => `Expected ${label} to be negative, got ${value}`, 152 | validator: value => value < 0, 153 | }); 154 | } 155 | 156 | /** 157 | Test a number to be an integer or infinite. 158 | */ 159 | get integerOrInfinite(): this { 160 | return this.addValidator({ 161 | message: (value, label) => `Expected ${label} to be an integer or infinite, got ${value}`, 162 | validator: value => is.integer(value) || is.infinite(value), 163 | }); 164 | } 165 | 166 | /** 167 | Test a number to be in a valid range for a 8-bit unsigned integer. 168 | */ 169 | get uint8(): this { 170 | return this.integer.inRange(0, 255); 171 | } 172 | 173 | /** 174 | Test a number to be in a valid range for a 16-bit unsigned integer. 175 | */ 176 | get uint16(): this { 177 | return this.integer.inRange(0, 65_535); 178 | } 179 | 180 | /** 181 | Test a number to be in a valid range for a 32-bit unsigned integer. 182 | */ 183 | get uint32(): this { 184 | return this.integer.inRange(0, 4_294_967_295); 185 | } 186 | 187 | /** 188 | Test a number to be in a valid range for a 8-bit signed integer. 189 | */ 190 | get int8(): this { 191 | return this.integer.inRange(-128, 127); 192 | } 193 | 194 | /** 195 | Test a number to be in a valid range for a 16-bit signed integer. 196 | */ 197 | get int16(): this { 198 | return this.integer.inRange(-32_768, 32_767); 199 | } 200 | 201 | /** 202 | Test a number to be in a valid range for a 32-bit signed integer. 203 | */ 204 | get int32(): this { 205 | return this.integer.inRange(-2_147_483_648, 2_147_483_647); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /source/predicates/object.ts: -------------------------------------------------------------------------------- 1 | import is from '@sindresorhus/is'; 2 | import {hasProperty} from 'dot-prop'; 3 | import {deepEqual} from 'fast-equals'; 4 | import hasItems from '../utils/has-items.js'; 5 | import ofType from '../utils/of-type.js'; 6 | import ofTypeDeep from '../utils/of-type-deep.js'; 7 | import { 8 | partial, 9 | exact, 10 | type Shape, 11 | type TypeOfShape, 12 | } from '../utils/match-shape.js'; 13 | import {Predicate, type PredicateOptions} from './predicate.js'; 14 | import type {BasePredicate} from './base-predicate.js'; 15 | 16 | export class ObjectPredicate extends Predicate { 17 | /** 18 | @hidden 19 | */ 20 | constructor(options?: PredicateOptions) { 21 | super('object', options); 22 | } 23 | 24 | /** 25 | Test if an Object is a plain object. 26 | */ 27 | get plain(): this { 28 | return this.addValidator({ 29 | message: (_, label) => `Expected ${label} to be a plain object`, 30 | validator: object => is.plainObject(object), 31 | }); 32 | } 33 | 34 | /** 35 | Test an object to be empty. 36 | */ 37 | get empty(): this { 38 | return this.addValidator({ 39 | message: (object, label) => `Expected ${label} to be empty, got \`${JSON.stringify(object)}\``, 40 | validator: object => Object.keys(object).length === 0, 41 | }); 42 | } 43 | 44 | /** 45 | Test an object to be not empty. 46 | */ 47 | get nonEmpty(): this { 48 | return this.addValidator({ 49 | message: (_, label) => `Expected ${label} to not be empty`, 50 | validator: object => Object.keys(object).length > 0, 51 | }); 52 | } 53 | 54 | /** 55 | Test all the values in the object to match the provided predicate. 56 | 57 | @param predicate - The predicate that should be applied against every value in the object. 58 | */ 59 | valuesOfType(predicate: BasePredicate): this { 60 | return this.addValidator({ 61 | message: (_, label, error) => `(${label}) ${error}`, 62 | validator: object => ofType(Object.values(object), 'values', predicate), 63 | }); 64 | } 65 | 66 | /** 67 | Test all the values in the object deeply to match the provided predicate. 68 | 69 | @param predicate - The predicate that should be applied against every value in the object. 70 | */ 71 | deepValuesOfType(predicate: Predicate): this { 72 | return this.addValidator({ 73 | message: (_, label, error) => `(${label}) ${error}`, 74 | validator: object => ofTypeDeep(object, predicate), 75 | }); 76 | } 77 | 78 | /** 79 | Test an object to be deeply equal to the provided object. 80 | 81 | @param expected - Expected object to match. 82 | */ 83 | deepEqual(expected: object): this { 84 | return this.addValidator({ 85 | message: (object, label) => `Expected ${label} to be deeply equal to \`${JSON.stringify(expected)}\`, got \`${JSON.stringify(object)}\``, 86 | validator: object => deepEqual(object, expected), 87 | }); 88 | } 89 | 90 | /** 91 | Test an object to be of a specific instance type. 92 | 93 | @param instance - The expected instance type of the object. 94 | */ 95 | instanceOf(instance: Function): this { 96 | return this.addValidator({ 97 | message(object: object, label: string) { 98 | let {name} = object?.constructor ?? {}; 99 | 100 | if (!name || name === 'Object') { 101 | name = JSON.stringify(object); 102 | } 103 | 104 | return `Expected ${label} \`${name}\` to be of type \`${instance.name}\``; 105 | }, 106 | validator: object => object instanceof instance, 107 | }); 108 | } 109 | 110 | /** 111 | Test an object to include all the provided keys. You can use [dot-notation](https://github.com/sindresorhus/dot-prop) in a key to access nested properties. 112 | 113 | @param keys - The keys that should be present in the object. 114 | */ 115 | hasKeys(...keys: readonly string[]): this { 116 | return this.addValidator({ 117 | message: (_, label, missingKeys) => `Expected ${label} to have keys \`${JSON.stringify(missingKeys)}\``, 118 | validator: object => hasItems( 119 | { 120 | has: item => hasProperty(object, item), 121 | }, 122 | keys, 123 | ), 124 | }); 125 | } 126 | 127 | /** 128 | Test an object to include any of the provided keys. You can use [dot-notation](https://github.com/sindresorhus/dot-prop) in a key to access nested properties. 129 | 130 | @param keys - The keys that could be a key in the object. 131 | */ 132 | hasAnyKeys(...keys: readonly string[]): this { 133 | return this.addValidator({ 134 | message: (_, label) => `Expected ${label} to have any key of \`${JSON.stringify(keys)}\``, 135 | validator: object => keys.some(key => hasProperty(object, key)), 136 | }); 137 | } 138 | 139 | /** 140 | Test an object to match the `shape` partially. This means that it ignores unexpected properties. The shape comparison is deep. 141 | 142 | The shape is an object which describes how the tested object should look like. The keys are the same as the source object and the values are predicates. 143 | 144 | @param shape - Shape to test the object against. 145 | 146 | @example 147 | ``` 148 | import ow from 'ow'; 149 | 150 | const object = { 151 | unicorn: '🦄', 152 | rainbow: '🌈' 153 | }; 154 | 155 | ow(object, ow.object.partialShape({ 156 | unicorn: ow.string 157 | })); 158 | ``` 159 | */ 160 | partialShape(shape: S): ObjectPredicate> { 161 | return this.addValidator({ 162 | // TODO: Improve this when message handling becomes smarter 163 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 164 | message: (_, label, message) => `${message.replace('Expected', 'Expected property')} in ${label}`, 165 | validator: object => partial(object, shape), 166 | }) as unknown as ObjectPredicate>; 167 | } 168 | 169 | /** 170 | Test an object to match the `shape` exactly. This means that will fail if it comes across unexpected properties. The shape comparison is deep. 171 | 172 | The shape is an object which describes how the tested object should look like. The keys are the same as the source object and the values are predicates. 173 | 174 | @param shape - Shape to test the object against. 175 | 176 | @example 177 | ``` 178 | import ow from 'ow'; 179 | 180 | ow({unicorn: '🦄'}, ow.object.exactShape({ 181 | unicorn: ow.string 182 | })); 183 | ``` 184 | */ 185 | exactShape(shape: S): ObjectPredicate> { 186 | // TODO [typescript@>=6] If higher-kinded types are supported natively by typescript, refactor `addValidator` to use them to avoid the usage of `any`. Otherwise, bump or remove this TODO. 187 | return this.addValidator({ 188 | // TODO: Improve this when message handling becomes smarter 189 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 190 | message: (_, label, message) => `${message.replace('Expected', 'Expected property')} in ${label}`, 191 | validator: object => exact(object, shape), 192 | }) as ObjectPredicate; 193 | } 194 | } 195 | 196 | export type {Shape} from '../utils/match-shape.js'; 197 | -------------------------------------------------------------------------------- /source/predicates/predicate.ts: -------------------------------------------------------------------------------- 1 | import is from '@sindresorhus/is'; 2 | import {ArgumentError} from '../argument-error.js'; 3 | import {not} from '../operators/not.js'; 4 | import type {Main} from '../index.js'; 5 | import {generateArgumentErrorMessage} from '../utils/generate-argument-error-message.js'; 6 | import {testSymbol, type BasePredicate} from './base-predicate.js'; 7 | 8 | /** 9 | Function executed when the provided validation fails. 10 | 11 | @param value - The tested value. 12 | @param label - Label of the tested value. 13 | 14 | @returns {string} - The actual error message. 15 | */ 16 | export type ValidatorMessageBuilder = (value: T, label?: string) => string; 17 | 18 | /** 19 | @hidden 20 | */ 21 | export type Validator = { 22 | message(value: T, label?: string, result?: any): string; 23 | 24 | validator(value: T): unknown; 25 | 26 | /** 27 | Provide custom message used by `not` operator. 28 | 29 | When absent, the return value of `message()` is used and 'not' is inserted after the first 'to', e.g. `Expected 'smth' to be empty` -> `Expected 'smth' to not be empty`. 30 | */ 31 | negatedMessage?(value: T, label: string): string; 32 | }; 33 | 34 | /** 35 | @hidden 36 | */ 37 | export type PredicateOptions = { 38 | optional?: boolean; 39 | }; 40 | 41 | /** 42 | @hidden 43 | */ 44 | export type Context = { 45 | validators: Array>; 46 | } & PredicateOptions; 47 | 48 | /** 49 | @hidden 50 | */ 51 | export const validatorSymbol = Symbol('validators'); 52 | 53 | export type CustomValidator = (value: T) => { 54 | /** 55 | Should be `true` if the validation is correct. 56 | */ 57 | validator: boolean; 58 | 59 | /** 60 | The error message which should be shown if the `validator` is `false`. Or a error function which returns the error message and accepts the label as first argument. 61 | */ 62 | message: string | ((label: string) => string); 63 | }; 64 | 65 | /** 66 | @hidden 67 | */ 68 | export class Predicate implements BasePredicate { 69 | private readonly context: Context = { 70 | validators: [], 71 | }; 72 | 73 | constructor( 74 | private readonly type: string, 75 | private readonly options: PredicateOptions = {}, 76 | ) { 77 | this.context = { 78 | ...this.context, 79 | ...this.options, 80 | }; 81 | 82 | const typeString = this.type.charAt(0).toLowerCase() + this.type.slice(1); 83 | 84 | this.addValidator({ 85 | message: (value, label) => { 86 | // We do not include type in this label as we do for other messages, because it would be redundant. 87 | const label_ = label?.slice(this.type.length + 1); 88 | 89 | // TODO: The NaN check can be removed when `@sindresorhus/is` is fixed: https://github.com/sindresorhus/ow/issues/231#issuecomment-1047100612 90 | // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing 91 | return `Expected ${label_ || 'argument'} to be of type \`${this.type}\` but received type \`${Number.isNaN(value) ? 'NaN' : is(value)}\``; 92 | }, 93 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 94 | validator: value => (is as any)[typeString](value), 95 | }); 96 | } 97 | 98 | /** 99 | @hidden 100 | */ 101 | [testSymbol](value: T, main: Main, label: string | (() => string), idLabel: boolean): asserts value is T { 102 | // Create a map of labels -> received errors. 103 | const errors = new Map>(); 104 | 105 | for (const {validator, message} of this.context.validators) { 106 | if (this.options.optional === true && value === undefined) { 107 | continue; 108 | } 109 | 110 | let result: unknown; 111 | 112 | try { 113 | result = validator(value); 114 | } catch (error: unknown) { 115 | // Any errors caught means validators couldn't process the input. 116 | result = error; 117 | } 118 | 119 | if (result === true) { 120 | continue; 121 | } 122 | 123 | const label2 = is.function_(label) ? label() : label; 124 | const labelWithTick = (label2 && idLabel) ? `\`${label2}\`` : label2; 125 | 126 | const label_ = labelWithTick 127 | ? `${this.type} ${labelWithTick}` 128 | : this.type; 129 | 130 | const mapKey = label2 || this.type; 131 | 132 | // Get the current errors encountered for this label. 133 | const currentErrors = errors.get(mapKey); 134 | // Pre-generate the error message that will be reported to the user. 135 | const errorMessage = message(value, label_, result); 136 | 137 | // If we already have any errors for this label. 138 | if (currentErrors) { 139 | // If we don't already have this error logged, add it. 140 | currentErrors.add(errorMessage); 141 | } else { 142 | // Set this label and error in the full map. 143 | errors.set(mapKey, new Set([errorMessage])); 144 | } 145 | } 146 | 147 | // If we have any errors to report, throw. 148 | if (errors.size > 0) { 149 | // Generate the `error.message` property. 150 | const message = generateArgumentErrorMessage(errors); 151 | 152 | throw new ArgumentError(message, main, errors); 153 | } 154 | } 155 | 156 | /** 157 | @hidden 158 | */ 159 | get [validatorSymbol](): Array> { 160 | return this.context.validators; 161 | } 162 | 163 | /** 164 | Invert the following validators. 165 | */ 166 | get not(): this { 167 | return not(this) as this; 168 | } 169 | 170 | /** 171 | Test if the value matches a custom validation function. The validation function should return an object containing a `validator` and `message`. If the `validator` is `false`, the validation fails and the `message` will be used as error message. If the `message` is a function, the function is invoked with the `label` as argument to let you further customize the error message. 172 | 173 | @param customValidator - Custom validation function. 174 | */ 175 | validate(customValidator: CustomValidator): this { 176 | return this.addValidator({ 177 | message: (_, label, error) => typeof error === 'string' 178 | ? `(${label}) ${error}` 179 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 180 | : error(label), 181 | validator(value) { 182 | const {message, validator} = customValidator(value); 183 | 184 | if (validator) { 185 | return true; 186 | } 187 | 188 | return message; 189 | }, 190 | }); 191 | } 192 | 193 | /** 194 | Test if the value matches a custom validation function. The validation function should return `true` if the value passes the function. If the function either returns `false` or a string, the function fails and the string will be used as error message. 195 | 196 | @param validator - Validation function. 197 | */ 198 | is(validator: (value: T) => boolean | string): this { 199 | return this.addValidator({ 200 | message: (value, label, error) => (error 201 | ? `(${label}) ${error}` 202 | : `Expected ${label} \`${value}\` to pass custom validation function` 203 | ), 204 | validator, 205 | }); 206 | } 207 | 208 | /** 209 | Provide a new error message to be thrown when the validation fails. 210 | 211 | @param newMessage - Either a string containing the new message or a function returning the new message. 212 | 213 | @example 214 | ``` 215 | ow('🌈', 'unicorn', ow.string.equals('🦄').message('Expected unicorn, got rainbow')); 216 | //=> ArgumentError: Expected unicorn, got rainbow 217 | ``` 218 | 219 | @example 220 | ``` 221 | ow('🌈', ow.string.minLength(5).message((value, label) => `Expected ${label}, to have a minimum length of 5, got \`${value}\``)); 222 | //=> ArgumentError: Expected string, to be have a minimum length of 5, got `🌈` 223 | ``` 224 | */ 225 | message(newMessage: string | ValidatorMessageBuilder): this { 226 | const {validators} = this.context; 227 | 228 | validators.at(-1)!.message = (value, label): string => { 229 | if (typeof newMessage === 'function') { 230 | return newMessage(value, label); 231 | } 232 | 233 | return newMessage; 234 | }; 235 | 236 | return this; 237 | } 238 | 239 | /** 240 | Register a new validator. 241 | 242 | @param validator - Validator to register. 243 | */ 244 | addValidator(validator: Validator): this { 245 | this.context.validators.push(validator); 246 | return this; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /source/predicates/set.ts: -------------------------------------------------------------------------------- 1 | import {deepEqual} from 'fast-equals'; 2 | import hasItems from '../utils/has-items.js'; 3 | import ofType from '../utils/of-type.js'; 4 | import {Predicate, type PredicateOptions} from './predicate.js'; 5 | 6 | export class SetPredicate extends Predicate> { 7 | /** 8 | @hidden 9 | */ 10 | constructor(options?: PredicateOptions) { 11 | super('Set', options); 12 | } 13 | 14 | /** 15 | Test a Set to have a specific size. 16 | 17 | @param size - The size of the Set. 18 | */ 19 | size(size: number): this { 20 | return this.addValidator({ 21 | message: (set, label) => `Expected ${label} to have size \`${size}\`, got \`${set.size}\``, 22 | validator: set => set.size === size, 23 | }); 24 | } 25 | 26 | /** 27 | Test a Set to have a minimum size. 28 | 29 | @param size - The minimum size of the Set. 30 | */ 31 | minSize(size: number): this { 32 | return this.addValidator({ 33 | message: (set, label) => `Expected ${label} to have a minimum size of \`${size}\`, got \`${set.size}\``, 34 | validator: set => set.size >= size, 35 | negatedMessage: (set, label) => `Expected ${label} to have a maximum size of \`${size - 1}\`, got \`${set.size}\``, 36 | }); 37 | } 38 | 39 | /** 40 | Test a Set to have a maximum size. 41 | 42 | @param size - The maximum size of the Set. 43 | */ 44 | maxSize(size: number): this { 45 | return this.addValidator({ 46 | message: (set, label) => `Expected ${label} to have a maximum size of \`${size}\`, got \`${set.size}\``, 47 | validator: set => set.size <= size, 48 | negatedMessage: (set, label) => `Expected ${label} to have a minimum size of \`${size + 1}\`, got \`${set.size}\``, 49 | }); 50 | } 51 | 52 | /** 53 | Test a Set to include all the provided items. The items are tested by identity, not structure. 54 | 55 | @param items - The items that should be a item in the Set. 56 | */ 57 | has(...items: readonly T[]): this { 58 | return this.addValidator({ 59 | message: (_, label, missingItems) => `Expected ${label} to have items \`${JSON.stringify(missingItems)}\``, 60 | validator: set => hasItems(set, items), 61 | }); 62 | } 63 | 64 | /** 65 | Test a Set to include any of the provided items. The items are tested by identity, not structure. 66 | 67 | @param items - The items that could be a item in the Set. 68 | */ 69 | hasAny(...items: readonly T[]): this { 70 | return this.addValidator({ 71 | message: (_, label) => `Expected ${label} to have any item of \`${JSON.stringify(items)}\``, 72 | validator: set => items.some(item => set.has(item)), 73 | }); 74 | } 75 | 76 | /** 77 | Test all the items in the Set to match the provided predicate. 78 | 79 | @param predicate - The predicate that should be applied against every item in the Set. 80 | */ 81 | ofType(predicate: Predicate): this { 82 | return this.addValidator({ 83 | message: (_, label, error) => `(${label}) ${error}`, 84 | validator: set => ofType(set, 'values', predicate), 85 | }); 86 | } 87 | 88 | /** 89 | Test a Set to be empty. 90 | */ 91 | get empty(): this { 92 | return this.addValidator({ 93 | message: (set, label) => `Expected ${label} to be empty, got \`${JSON.stringify([...set])}\``, 94 | validator: set => set.size === 0, 95 | }); 96 | } 97 | 98 | /** 99 | Test a Set to be not empty. 100 | */ 101 | get nonEmpty(): this { 102 | return this.addValidator({ 103 | message: (_, label) => `Expected ${label} to not be empty`, 104 | validator: set => set.size > 0, 105 | }); 106 | } 107 | 108 | /** 109 | Test a Set to be deeply equal to the provided Set. 110 | 111 | @param expected - Expected Set to match. 112 | */ 113 | deepEqual(expected: Set): this { 114 | return this.addValidator({ 115 | message: (set, label) => `Expected ${label} to be deeply equal to \`${JSON.stringify([...expected])}\`, got \`${JSON.stringify([...set])}\``, 116 | validator: set => deepEqual(set, expected), 117 | }); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /source/predicates/string.ts: -------------------------------------------------------------------------------- 1 | import is from '@sindresorhus/is'; 2 | import {Predicate, type PredicateOptions} from './predicate.js'; 3 | 4 | export class StringPredicate extends Predicate { 5 | /** 6 | @hidden 7 | */ 8 | constructor(options?: PredicateOptions) { 9 | super('string', options); 10 | } 11 | 12 | /** 13 | Test a string to have a specific length. 14 | 15 | @param length - The length of the string. 16 | */ 17 | length(length: number): this { 18 | return this.addValidator({ 19 | message: (value, label) => `Expected ${label} to have length \`${length}\`, got \`${value}\``, 20 | validator: value => value.length === length, 21 | }); 22 | } 23 | 24 | /** 25 | Test a string to have a minimum length. 26 | 27 | @param length - The minimum length of the string. 28 | */ 29 | minLength(length: number): this { 30 | return this.addValidator({ 31 | message: (value, label) => `Expected ${label} to have a minimum length of \`${length}\`, got \`${value}\``, 32 | validator: value => value.length >= length, 33 | negatedMessage: (value, label) => `Expected ${label} to have a maximum length of \`${length - 1}\`, got \`${value}\``, 34 | }); 35 | } 36 | 37 | /** 38 | Test a string to have a maximum length. 39 | 40 | @param length - The maximum length of the string. 41 | */ 42 | maxLength(length: number): this { 43 | return this.addValidator({ 44 | message: (value, label) => `Expected ${label} to have a maximum length of \`${length}\`, got \`${value}\``, 45 | validator: value => value.length <= length, 46 | negatedMessage: (value, label) => `Expected ${label} to have a minimum length of \`${length + 1}\`, got \`${value}\``, 47 | }); 48 | } 49 | 50 | /** 51 | Test a string against a regular expression. 52 | 53 | @param regex - The regular expression to match the value with. 54 | */ 55 | matches(regex: RegExp): this { 56 | return this.addValidator({ 57 | message: (value, label) => `Expected ${label} to match \`${regex}\`, got \`${value}\``, 58 | validator: value => regex.test(value), 59 | }); 60 | } 61 | 62 | /** 63 | Test a string to start with a specific value. 64 | 65 | @param searchString - The value that should be the start of the string. 66 | */ 67 | startsWith(searchString: string): this { 68 | return this.addValidator({ 69 | message: (value, label) => `Expected ${label} to start with \`${searchString}\`, got \`${value}\``, 70 | validator: value => value.startsWith(searchString), 71 | }); 72 | } 73 | 74 | /** 75 | Test a string to end with a specific value. 76 | 77 | @param searchString - The value that should be the end of the string. 78 | */ 79 | endsWith(searchString: string): this { 80 | return this.addValidator({ 81 | message: (value, label) => `Expected ${label} to end with \`${searchString}\`, got \`${value}\``, 82 | validator: value => value.endsWith(searchString), 83 | }); 84 | } 85 | 86 | /** 87 | Test a string to include a specific value. 88 | 89 | @param searchString - The value that should be included in the string. 90 | */ 91 | includes(searchString: string): this { 92 | return this.addValidator({ 93 | message: (value, label) => `Expected ${label} to include \`${searchString}\`, got \`${value}\``, 94 | validator: value => value.includes(searchString), 95 | }); 96 | } 97 | 98 | /** 99 | Test if the string is an element of the provided list. 100 | 101 | @param list - List of possible values. 102 | */ 103 | oneOf(list: readonly string[]): this { 104 | return this.addValidator({ 105 | message(value, label) { 106 | let printedList = JSON.stringify(list); 107 | 108 | if (list.length > 10) { 109 | const overflow = list.length - 10; 110 | printedList = JSON.stringify(list.slice(0, 10)).replace(/]$/, `,…+${overflow} more]`); 111 | } 112 | 113 | return `Expected ${label} to be one of \`${printedList}\`, got \`${value}\``; 114 | }, 115 | validator: value => list.includes(value), 116 | }); 117 | } 118 | 119 | /** 120 | Test a string to be empty. 121 | */ 122 | get empty(): this { 123 | return this.addValidator({ 124 | message: (value, label) => `Expected ${label} to be empty, got \`${value}\``, 125 | validator: value => value === '', 126 | }); 127 | } 128 | 129 | /** 130 | Test a string to contain at least 1 non-whitespace character. 131 | */ 132 | get nonBlank(): this { 133 | return this.addValidator({ 134 | message(value, label) { 135 | // Unicode's formal substitute characters can be barely legible and may not be easily recognized. 136 | // Hence this alternative substitution scheme. 137 | const madeVisible = value 138 | .replaceAll(' ', '·') 139 | .replaceAll('\f', '\\f') 140 | .replaceAll('\n', '\\n') 141 | .replaceAll('\r', '\\r') 142 | .replaceAll('\t', '\\t') 143 | .replaceAll('\v', '\\v'); 144 | return `Expected ${label} to not be only whitespace, got \`${madeVisible}\``; 145 | }, 146 | validator: value => value.trim() !== '', 147 | }); 148 | } 149 | 150 | /** 151 | Test a string to be not empty. 152 | */ 153 | get nonEmpty(): this { 154 | return this.addValidator({ 155 | message: (_, label) => `Expected ${label} to not be empty`, 156 | validator: value => value !== '', 157 | }); 158 | } 159 | 160 | /** 161 | Test a string to be equal to a specified string. 162 | 163 | @param expected - Expected value to match. 164 | */ 165 | equals(expected: string): this { 166 | return this.addValidator({ 167 | message: (value, label) => `Expected ${label} to be equal to \`${expected}\`, got \`${value}\``, 168 | validator: value => value === expected, 169 | }); 170 | } 171 | 172 | /** 173 | Test a string to be alphanumeric. 174 | */ 175 | get alphanumeric(): this { 176 | return this.addValidator({ 177 | message: (value, label) => `Expected ${label} to be alphanumeric, got \`${value}\``, 178 | validator: value => /^[a-z\d]+$/i.test(value), 179 | }); 180 | } 181 | 182 | /** 183 | Test a string to be alphabetical. 184 | */ 185 | get alphabetical(): this { 186 | return this.addValidator({ 187 | message: (value, label) => `Expected ${label} to be alphabetical, got \`${value}\``, 188 | validator: value => /^[a-z]+$/gi.test(value), 189 | }); 190 | } 191 | 192 | /** 193 | Test a string to be numeric. 194 | */ 195 | get numeric(): this { 196 | return this.addValidator({ 197 | message: (value, label) => `Expected ${label} to be numeric, got \`${value}\``, 198 | validator: value => /^[+-]?\d+$/i.test(value), 199 | }); 200 | } 201 | 202 | /** 203 | Test a string to be a valid date. 204 | */ 205 | get date(): this { 206 | return this.addValidator({ 207 | message: (value, label) => `Expected ${label} to be a date, got \`${value}\``, 208 | validator: value => is.validDate(new Date(value)), 209 | }); 210 | } 211 | 212 | /** 213 | Test a non-empty string to be lowercase. Matching both alphabetical & numbers. 214 | */ 215 | get lowercase(): this { 216 | return this.addValidator({ 217 | message: (value, label) => `Expected ${label} to be lowercase, got \`${value}\``, 218 | validator: value => value.trim() !== '' && value === value.toLowerCase(), 219 | }); 220 | } 221 | 222 | /** 223 | Test a non-empty string to be uppercase. Matching both alphabetical & numbers. 224 | */ 225 | get uppercase(): this { 226 | return this.addValidator({ 227 | message: (value, label) => `Expected ${label} to be uppercase, got \`${value}\``, 228 | validator: value => value.trim() !== '' && value === value.toUpperCase(), 229 | }); 230 | } 231 | 232 | /** 233 | Test a string to be a valid URL. 234 | */ 235 | get url(): this { 236 | return this.addValidator({ 237 | message: (value, label) => `Expected ${label} to be a URL, got \`${value}\``, 238 | validator: is.urlString, 239 | }); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /source/predicates/typed-array.ts: -------------------------------------------------------------------------------- 1 | import type {TypedArray} from '../typed-array.js'; 2 | import {Predicate} from './predicate.js'; 3 | 4 | export class TypedArrayPredicate extends Predicate { 5 | /** 6 | Test a typed array to have a specific byte length. 7 | 8 | @param byteLength - The byte length of the typed array. 9 | */ 10 | byteLength(byteLength: number): this { 11 | return this.addValidator({ 12 | message: (value, label) => `Expected ${label} to have byte length of \`${byteLength}\`, got \`${value.byteLength}\``, 13 | validator: value => value.byteLength === byteLength, 14 | }); 15 | } 16 | 17 | /** 18 | Test a typed array to have a minimum byte length. 19 | 20 | @param byteLength - The minimum byte length of the typed array. 21 | */ 22 | minByteLength(byteLength: number): this { 23 | return this.addValidator({ 24 | message: (value, label) => `Expected ${label} to have a minimum byte length of \`${byteLength}\`, got \`${value.byteLength}\``, 25 | validator: value => value.byteLength >= byteLength, 26 | negatedMessage: (value, label) => `Expected ${label} to have a maximum byte length of \`${byteLength - 1}\`, got \`${value.byteLength}\``, 27 | }); 28 | } 29 | 30 | /** 31 | Test a typed array to have a minimum byte length. 32 | 33 | @param length - The minimum byte length of the typed array. 34 | */ 35 | maxByteLength(byteLength: number): this { 36 | return this.addValidator({ 37 | message: (value, label) => `Expected ${label} to have a maximum byte length of \`${byteLength}\`, got \`${value.byteLength}\``, 38 | validator: value => value.byteLength <= byteLength, 39 | negatedMessage: (value, label) => `Expected ${label} to have a minimum byte length of \`${byteLength + 1}\`, got \`${value.byteLength}\``, 40 | }); 41 | } 42 | 43 | /** 44 | Test a typed array to have a specific length. 45 | 46 | @param length - The length of the typed array. 47 | */ 48 | length(length: number): this { 49 | return this.addValidator({ 50 | message: (value, label) => `Expected ${label} to have length \`${length}\`, got \`${value.length}\``, 51 | validator: value => value.length === length, 52 | }); 53 | } 54 | 55 | /** 56 | Test a typed array to have a minimum length. 57 | 58 | @param length - The minimum length of the typed array. 59 | */ 60 | minLength(length: number): this { 61 | return this.addValidator({ 62 | message: (value, label) => `Expected ${label} to have a minimum length of \`${length}\`, got \`${value.length}\``, 63 | validator: value => value.length >= length, 64 | negatedMessage: (value, label) => `Expected ${label} to have a maximum length of \`${length - 1}\`, got \`${value.length}\``, 65 | }); 66 | } 67 | 68 | /** 69 | Test a typed array to have a maximum length. 70 | 71 | @param length - The maximum length of the typed array. 72 | */ 73 | maxLength(length: number): this { 74 | return this.addValidator({ 75 | message: (value, label) => `Expected ${label} to have a maximum length of \`${length}\`, got \`${value.length}\``, 76 | validator: value => value.length <= length, 77 | negatedMessage: (value, label) => `Expected ${label} to have a minimum length of \`${length + 1}\`, got \`${value.length}\``, 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /source/predicates/weak-map.ts: -------------------------------------------------------------------------------- 1 | import hasItems from '../utils/has-items.js'; 2 | import {Predicate, type PredicateOptions} from './predicate.js'; 3 | 4 | export class WeakMapPredicate extends Predicate> { 5 | /** 6 | @hidden 7 | */ 8 | constructor(options?: PredicateOptions) { 9 | super('WeakMap', options); 10 | } 11 | 12 | /** 13 | Test a WeakMap to include all the provided keys. The keys are tested by identity, not structure. 14 | 15 | @param keys - The keys that should be a key in the WeakMap. 16 | */ 17 | hasKeys(...keys: readonly KeyType[]): this { 18 | return this.addValidator({ 19 | message: (_, label, missingKeys) => `Expected ${label} to have keys \`${JSON.stringify(missingKeys)}\``, 20 | validator: map => hasItems(map, keys), 21 | }); 22 | } 23 | 24 | /** 25 | Test a WeakMap to include any of the provided keys. The keys are tested by identity, not structure. 26 | 27 | @param keys - The keys that could be a key in the WeakMap. 28 | */ 29 | hasAnyKeys(...keys: readonly KeyType[]): this { 30 | return this.addValidator({ 31 | message: (_, label) => `Expected ${label} to have any key of \`${JSON.stringify(keys)}\``, 32 | validator: map => keys.some(key => map.has(key)), 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /source/predicates/weak-set.ts: -------------------------------------------------------------------------------- 1 | import hasItems from '../utils/has-items.js'; 2 | import {Predicate, type PredicateOptions} from './predicate.js'; 3 | 4 | export class WeakSetPredicate extends Predicate> { 5 | /** 6 | @hidden 7 | */ 8 | constructor(options?: PredicateOptions) { 9 | super('WeakSet', options); 10 | } 11 | 12 | /** 13 | Test a WeakSet to include all the provided items. The items are tested by identity, not structure. 14 | 15 | @param items - The items that should be a item in the WeakSet. 16 | */ 17 | has(...items: readonly T[]): this { 18 | return this.addValidator({ 19 | message: (_, label, missingItems) => `Expected ${label} to have items \`${JSON.stringify(missingItems)}\``, 20 | validator: set => hasItems(set, items), 21 | }); 22 | } 23 | 24 | /** 25 | Test a WeakSet to include any of the provided items. The items are tested by identity, not structure. 26 | 27 | @param items - The items that could be a item in the WeakSet. 28 | */ 29 | hasAny(...items: readonly T[]): this { 30 | return this.addValidator({ 31 | message: (_, label) => `Expected ${label} to have any item of \`${JSON.stringify(items)}\``, 32 | validator: set => items.some(item => set.has(item)), 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /source/test.ts: -------------------------------------------------------------------------------- 1 | import {testSymbol, type BasePredicate} from './predicates/base-predicate.js'; 2 | 3 | /** 4 | Validate the value against the provided predicate. 5 | 6 | @hidden 7 | 8 | @param value - Value to test. 9 | @param label - Label which should be used in error messages. 10 | @param predicate - Predicate to test to value against. 11 | @param idLabel - If true, the label is a variable or type. Default: true. 12 | */ 13 | export default function test(value: T, label: string | Function, predicate: BasePredicate, idLabel = true): void { 14 | predicate[testSymbol](value, test, label, idLabel); 15 | } 16 | -------------------------------------------------------------------------------- /source/typed-array.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/sindresorhus/type-fest/blob/main/source/typed-array.d.ts 2 | export type TypedArray = 3 | | Int8Array 4 | | Uint8Array 5 | | Uint8ClampedArray 6 | | Int16Array 7 | | Uint16Array 8 | | Int32Array 9 | | Uint32Array 10 | | Float32Array 11 | | Float64Array 12 | | BigInt64Array 13 | | BigUint64Array; 14 | -------------------------------------------------------------------------------- /source/utils/generate-argument-error-message.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Generates a complete message from all errors generated by predicates. 3 | 4 | @param errors - The errors generated by the predicates. 5 | @param isAny - If this function is called from the any argument. 6 | @hidden 7 | */ 8 | export const generateArgumentErrorMessage = (errors: Map>, isAny = false): string => { 9 | const message = []; 10 | 11 | const errorArray = [...errors.entries()]; 12 | 13 | const anyErrorWithoutOneItemOnly = errorArray.some(([, array]) => array.size !== 1); 14 | 15 | // If only one error "key" is present, enumerate all of those errors only. 16 | if (errorArray.length === 1) { 17 | const [, returnedErrors] = errorArray[0]!; 18 | 19 | if (!isAny && returnedErrors.size === 1) { 20 | const [errorMessage] = returnedErrors; 21 | return errorMessage!; 22 | } 23 | 24 | for (const entry of returnedErrors) { 25 | message.push(`${isAny ? ' - ' : ''}${entry}`); 26 | } 27 | 28 | return message.join('\n'); 29 | } 30 | 31 | // If every predicate returns just one error, enumerate them as is. 32 | if (!anyErrorWithoutOneItemOnly) { 33 | return errorArray.map(([, [item]]) => ` - ${item}`).join('\n'); 34 | } 35 | 36 | // Else, iterate through all the errors and enumerate them. 37 | for (const [key, value] of errorArray) { 38 | message.push(`Errors from the "${key}" predicate:`); 39 | for (const entry of value) { 40 | message.push(` - ${entry}`); 41 | } 42 | } 43 | 44 | return message.join('\n'); 45 | }; 46 | -------------------------------------------------------------------------------- /source/utils/generate-stack.ts: -------------------------------------------------------------------------------- 1 | /** 2 | Generates a useful stacktrace that points to the user's code where the error happened on platforms without the `Error.captureStackTrace()` method. 3 | 4 | @hidden 5 | */ 6 | export const generateStackTrace = (): string => { 7 | const stack = new RangeError('INTERNAL_OW_ERROR').stack!; 8 | 9 | return stack; 10 | }; 11 | -------------------------------------------------------------------------------- /source/utils/has-items.ts: -------------------------------------------------------------------------------- 1 | /** 2 | @hidden 3 | */ 4 | export type CollectionLike = { 5 | has: (item: T) => boolean; 6 | }; 7 | 8 | /** 9 | Retrieve the missing values in a collection based on an array of items. 10 | 11 | @hidden 12 | 13 | @param source - Source collection to search through. 14 | @param items - Items to search for. 15 | @param maxValues - Maximum number of values after the search process is stopped. Default: 5. 16 | */ 17 | const hasItems = (source: CollectionLike, items: readonly T[], maxValues = 5): true | T[] => { 18 | const missingValues: T[] = []; 19 | 20 | for (const value of items) { 21 | if (source.has(value)) { 22 | continue; 23 | } 24 | 25 | missingValues.push(value); 26 | 27 | if (missingValues.length === maxValues) { 28 | return missingValues; 29 | } 30 | } 31 | 32 | return missingValues.length === 0 ? true : missingValues; 33 | }; 34 | 35 | export default hasItems; 36 | -------------------------------------------------------------------------------- /source/utils/infer-label.browser.ts: -------------------------------------------------------------------------------- 1 | export const inferLabel = () => {}; 2 | -------------------------------------------------------------------------------- /source/utils/infer-label.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import type {CallSite} from 'callsites'; 3 | import {isNode} from 'environment'; 4 | import isIdentifier from 'is-identifier'; 5 | 6 | // Regex to extract the label out of the `ow` function call 7 | const labelRegex = /^.*?\((?