├── .gitignore ├── .vscode └── settings.json ├── README.md ├── tsconfig.json ├── LICENSE ├── package.json ├── eslint.config.ts ├── test └── index.test.ts └── src └── validators.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.options": { 3 | "flags": ["unstable_ts_config"] 4 | } 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ts-validators 2 | 3 | 4 | A collection of validator functions I frequently use and I'm putting here for sharing purposes. There's now a library version of this repo jet-validators. 5 |
6 | 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "ES2020" 7 | ], 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "strict": true, 11 | "skipLibCheck": true 12 | }, 13 | "include": [ 14 | "./src", 15 | "./test", 16 | "eslint.config.ts" 17 | ], 18 | "exclude": [ 19 | "./node_modules" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sean Maxwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ts-validators", 3 | "version": "1.0.0", 4 | "description": "A collection of commonly used validator function for TypeScript", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "vitest", 8 | "clean-install": "rm -rf ./node_modules && rm -r package-lock.json && npm i", 9 | "lint": "eslint --flag unstable_ts_config" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/seanpmaxwell/ts-validators.git" 14 | }, 15 | "keywords": [ 16 | "typescript", 17 | "types", 18 | "validators", 19 | "jet-schema", 20 | "schema", 21 | "type", 22 | "predicate", 23 | "validate", 24 | "check" 25 | ], 26 | "author": "sean maxwell", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/seanpmaxwell/ts-validators/issues" 30 | }, 31 | "homepage": "https://github.com/seanpmaxwell/ts-validators#readme", 32 | "devDependencies": { 33 | "@eslint/js": "^9.11.1", 34 | "@stylistic/eslint-plugin-ts": "^2.8.0", 35 | "@types/eslint__js": "^8.42.3", 36 | "@types/node": "^22.8.1", 37 | "eslint": "^9.11.1", 38 | "eslint-plugin-n": "^17.10.3", 39 | "jiti": "^2.3.3", 40 | "ts-node": "^10.9.2", 41 | "tslib": "^2.8.0", 42 | "typescript": "^5.6.3", 43 | "typescript-eslint": "^8.8.0", 44 | "vitest": "^2.1.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import tseslint from 'typescript-eslint'; 3 | import stylisticTs from '@stylistic/eslint-plugin-ts'; 4 | import nodePlugin from 'eslint-plugin-n'; 5 | 6 | 7 | export default tseslint.config( 8 | eslint.configs.recommended, 9 | nodePlugin.configs['flat/recommended-script'], 10 | ...tseslint.configs.strictTypeChecked, 11 | ...tseslint.configs.stylisticTypeChecked, 12 | { 13 | ignores: [ 14 | '**/node_modules/*', 15 | '**/*.mjs', 16 | '**/*.js', 17 | '**/dist', 18 | ], 19 | }, 20 | { 21 | languageOptions: { 22 | parserOptions: { 23 | project: './tsconfig.json', 24 | warnOnUnsupportedTypeScriptVersion: false, 25 | }, 26 | }, 27 | }, 28 | { 29 | plugins: { 30 | '@stylistic/ts': stylisticTs, 31 | }, 32 | }, 33 | { 34 | files: ['**/*.ts'], 35 | }, 36 | { 37 | rules: { 38 | '@typescript-eslint/explicit-member-accessibility': 'warn', 39 | '@typescript-eslint/no-misused-promises': 0, 40 | '@typescript-eslint/no-floating-promises': 0, 41 | '@typescript-eslint/no-confusing-void-expression': 0, 42 | '@typescript-eslint/no-unnecessary-condition': 0, 43 | '@typescript-eslint/restrict-template-expressions': [ 44 | 'error', { allowNumber: true }, 45 | ], 46 | '@typescript-eslint/restrict-plus-operands': [ 47 | 'warn', { allowNumberAndString: true }, 48 | ], 49 | '@typescript-eslint/no-unused-vars': 'warn', 50 | '@typescript-eslint/no-unsafe-enum-comparison': 0, 51 | '@typescript-eslint/no-unnecessary-type-parameters': 0, 52 | 'max-len': [ 53 | 'warn', 54 | { 55 | 'code': 80, 56 | }, 57 | ], 58 | '@stylistic/ts/semi': ['warn'], 59 | '@typescript-eslint/no-non-null-assertion': 0, 60 | '@typescript-eslint/no-unused-expressions': 'warn', 61 | 'comma-dangle': ['warn', 'always-multiline'], 62 | 'no-console': 1, 63 | 'no-extra-boolean-cast': 0, 64 | 'indent': ['warn', 2], 65 | 'quotes': ['warn', 'single'], 66 | 'n/no-process-env': 1, 67 | 'n/no-missing-import': 0, 68 | 'n/no-unpublished-import': 0, 69 | 'prefer-const': 'warn', 70 | }, 71 | }, 72 | ); 73 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | 3 | import { 4 | isNulBoolArr, 5 | isEnumVal, 6 | isUndef, 7 | isNull, 8 | isNoU, 9 | isBool, 10 | isOptBool, 11 | isNulBool, 12 | isNishBool, 13 | isBoolArr, 14 | isOptBoolArr, 15 | isNishBoolArr, 16 | isNum, 17 | isOptNum, 18 | isNulNum, 19 | isNishNum, 20 | isNumArr, 21 | isOptNumArr, 22 | isNulNumArr, 23 | isNishNumArr, 24 | isStr, 25 | isOptStr, 26 | isNulStr, 27 | isNishStr, 28 | isStrArr, 29 | isOptStrArr, 30 | isNulStrArr, 31 | isNishStrArr, 32 | isDate, 33 | isOptDate, 34 | isNulDate, 35 | isNishDate, 36 | isDateArr, 37 | isOptDateArr, 38 | isNulDateArr, 39 | isNishDateArr, 40 | isObj, 41 | isOptObj, 42 | isNulObj, 43 | isNishObj, 44 | isObjArr, 45 | isOptObjArr, 46 | isNulObjArr, 47 | isNishObjArr, 48 | isFuncArr, 49 | isOptFuncArr, 50 | isNishFuncArr, 51 | isNulFuncArr, 52 | isNishFunc, 53 | isNulFunc, 54 | isOptFunc, 55 | isFunc, 56 | isColor, 57 | isOptColor, 58 | isNulColor, 59 | isNishColor, 60 | isEmail, 61 | isOptEmail, 62 | isNishEmail, 63 | isNulEmail, 64 | isAlphaNumStr, 65 | isOptAlphaNumStr, 66 | isNulAlphaNumStr, 67 | isNishAlphaNumStr, 68 | isInArr, 69 | isOptOrInArr, 70 | nonNullable, 71 | isBasicObj, 72 | iterateObjEntries, 73 | isEnum, 74 | isNishEnumVal, 75 | isOptEnumVal, 76 | isNulEnumVal, 77 | isNishOrInArr, 78 | isNeStr, 79 | isOptNeStr, 80 | isNulNeStr, 81 | isNishNeStr, 82 | isInRange, 83 | isNishInRange, 84 | isOptInRange, 85 | isNulInRangeArr, 86 | isKeyOf, 87 | isNulKeyOfArr, 88 | transform, 89 | parseObj, 90 | parseObjArr, 91 | parseOptObj, 92 | parseNishObjArr, 93 | isValidNum, 94 | isValidDate, 95 | isValidBool, 96 | isBigInt, 97 | isOptBigInt, 98 | safeJsonParse, 99 | testObj, 100 | isSymbol, 101 | isOptSymbol, 102 | isNulSymbol, 103 | isNishSymbol, 104 | } from '../src/validators'; 105 | 106 | 107 | /** 108 | * Test all the basic validators 109 | */ 110 | test('test User all default values', () => { 111 | 112 | // Nullables 113 | expect(isUndef(undefined)).toStrictEqual(true); 114 | expect(isNull(null)).toStrictEqual(true); 115 | expect(isNoU(null)).toStrictEqual(true); 116 | expect(isNoU(undefined)).toStrictEqual(true); 117 | 118 | // Booleans 119 | expect(isBool(false)).toStrictEqual(true); 120 | expect(isBool('asdf')).toStrictEqual(false); 121 | expect(isOptBool(false)).toStrictEqual(true); 122 | expect(isOptBool(undefined)).toStrictEqual(true); 123 | expect(isNulBool(false)).toStrictEqual(true); 124 | expect(isNulBool(null)).toStrictEqual(true); 125 | expect(isNishBool(false)).toStrictEqual(true); 126 | expect(isNishBool(null)).toStrictEqual(true); 127 | expect(isNishBool(undefined)).toStrictEqual(true); 128 | 129 | // Is valid boolean 130 | expect(isValidBool(false)).toStrictEqual(true); 131 | expect(isValidBool(true)).toStrictEqual(true); 132 | expect(isValidBool('Yes')).toStrictEqual(true); 133 | expect(isValidBool('no')).toStrictEqual(true); 134 | expect(isValidBool('1')).toStrictEqual(true); 135 | expect(isValidBool('0')).toStrictEqual(true); 136 | expect(isValidBool(1)).toStrictEqual(true); 137 | expect(isValidBool(0)).toStrictEqual(true); 138 | expect(isValidBool('False')).toStrictEqual(true); 139 | expect(isValidBool('tRuE')).toStrictEqual(true); 140 | expect(isValidBool(1234)).toStrictEqual(false); 141 | expect(isValidBool(undefined)).toStrictEqual(false); 142 | 143 | // Boolean Arrays 144 | expect(isBoolArr([false, true, false])).toStrictEqual(true); 145 | expect(isBoolArr([false, true, 'asdf'])).toStrictEqual(false); 146 | expect(isBoolArr(true)).toStrictEqual(false); 147 | expect(isOptBoolArr([false, true, false])).toStrictEqual(true); 148 | expect(isOptBoolArr(undefined)).toStrictEqual(true); 149 | expect(isNulBoolArr([false, true, false])).toStrictEqual(true); 150 | expect(isNulBoolArr(null)).toStrictEqual(true); 151 | expect(isNishBoolArr([false, true, false])).toStrictEqual(true); 152 | expect(isNishBoolArr(null)).toStrictEqual(true); 153 | expect(isNishBoolArr(undefined)).toStrictEqual(true); 154 | 155 | // Numbers 156 | expect(isNum(123)).toStrictEqual(true); 157 | expect(isNum(false)).toStrictEqual(false); 158 | expect(isOptNum(123)).toStrictEqual(true); 159 | expect(isOptNum(undefined)).toStrictEqual(true); 160 | expect(isNulNum(123)).toStrictEqual(true); 161 | expect(isNulNum(null)).toStrictEqual(true); 162 | expect(isNishNum(123)).toStrictEqual(true); 163 | expect(isNishNum(null)).toStrictEqual(true); 164 | expect(isNishNum(undefined)).toStrictEqual(true); 165 | 166 | // Valid numbers 167 | expect(isValidNum('123')).toStrictEqual(true); 168 | 169 | // Number Arrays 170 | expect(isNumArr([1, 2, 3])).toStrictEqual(true); 171 | expect(isNumArr([false, true, '123'])).toStrictEqual(false); 172 | expect(isNumArr(123)).toStrictEqual(false); 173 | expect(isOptNumArr([1, 2 ,3])).toStrictEqual(true); 174 | expect(isOptNumArr(undefined)).toStrictEqual(true); 175 | expect(isNulNumArr([1, 2, 3])).toStrictEqual(true); 176 | expect(isNulNumArr(null)).toStrictEqual(true); 177 | expect(isNishNumArr([1, 2, 3])).toStrictEqual(true); 178 | expect(isNishNumArr(null)).toStrictEqual(true); 179 | expect(isNishNumArr(undefined)).toStrictEqual(true); 180 | 181 | // BigInt 182 | expect(isBigInt(1234567890123456789012345n)).toStrictEqual(true); 183 | expect(isOptBigInt(undefined)).toStrictEqual(true); 184 | 185 | // Ranges 186 | const isValidAge = isInRange(18, 130); 187 | expect(isValidAge(123)).toStrictEqual(true); 188 | expect(isValidAge(5)).toStrictEqual(false); 189 | expect(isValidAge(150)).toStrictEqual(false); 190 | const isPos = isNishInRange(0, null); 191 | expect(isPos(1_000_000)).toStrictEqual(true); 192 | expect(isPos(-1)).toStrictEqual(false); 193 | expect(isPos(undefined)).toStrictEqual(true); 194 | expect(isPos(null)).toStrictEqual(true); 195 | const isNeg = isOptInRange(null, -.000001); 196 | expect(isNeg(-1_000_000)).toStrictEqual(true); 197 | expect(isNeg(.01)).toStrictEqual(false); 198 | expect(isNeg(undefined)).toStrictEqual(true); 199 | expect(isNeg(null)).toStrictEqual(false); 200 | const isValidNums = isNulInRangeArr(-1, 10); 201 | expect(isValidNums([-1, 2, 3])).toStrictEqual(true); 202 | expect(isValidNums([-1, 11, 3])).toStrictEqual(false); 203 | expect(isValidNums([-1, null, 3])).toStrictEqual(false); 204 | expect(isValidNums(2)).toStrictEqual(false); 205 | expect(isValidNums(null)).toStrictEqual(true); 206 | 207 | // Strings 208 | expect(isStr('123')).toStrictEqual(true); 209 | expect(isStr(false)).toStrictEqual(false); 210 | expect(isOptStr('123')).toStrictEqual(true); 211 | expect(isOptStr(undefined)).toStrictEqual(true); 212 | expect(isNulStr('123')).toStrictEqual(true); 213 | expect(isNulStr(null)).toStrictEqual(true); 214 | expect(isNishStr('123')).toStrictEqual(true); 215 | expect(isNishStr(null)).toStrictEqual(true); 216 | expect(isNishStr(undefined)).toStrictEqual(true); 217 | 218 | // Non-Empty Strings 219 | expect(isNeStr('123')).toStrictEqual(true); 220 | expect(isNeStr('')).toStrictEqual(false); 221 | expect(isOptNeStr('123')).toStrictEqual(true); 222 | expect(isOptNeStr(undefined)).toStrictEqual(true); 223 | expect(isNulNeStr('123')).toStrictEqual(true); 224 | expect(isNulNeStr(null)).toStrictEqual(true); 225 | expect(isNishNeStr('123')).toStrictEqual(true); 226 | expect(isNishNeStr('')).toStrictEqual(false); 227 | expect(isNishNeStr(null)).toStrictEqual(true); 228 | expect(isNishNeStr(undefined)).toStrictEqual(true); 229 | 230 | // String Arrays 231 | expect(isStrArr(['1', '2', '3'])).toStrictEqual(true); 232 | expect(isStrArr(['false', '123', true])).toStrictEqual(false); 233 | expect(isStrArr('123')).toStrictEqual(false); 234 | expect(isOptStrArr(['1', '2', '3'])).toStrictEqual(true); 235 | expect(isOptStrArr(undefined)).toStrictEqual(true); 236 | expect(isNulStrArr(['1', '2', '3'])).toStrictEqual(true); 237 | expect(isNulStrArr(null)).toStrictEqual(true); 238 | expect(isNishStrArr(['1', '2', '3'])).toStrictEqual(true); 239 | expect(isNishStrArr(null)).toStrictEqual(true); 240 | expect(isNishStrArr(undefined)).toStrictEqual(true); 241 | 242 | // Symbol 243 | expect(isSymbol(Symbol('foo'))).toStrictEqual(true); 244 | expect(isSymbol(false)).toStrictEqual(false); 245 | expect(isOptSymbol(Symbol('foo'))).toStrictEqual(true); 246 | expect(isOptSymbol(undefined)).toStrictEqual(true); 247 | expect(isNulSymbol(Symbol('foo'))).toStrictEqual(true); 248 | expect(isNulSymbol(null)).toStrictEqual(true); 249 | expect(isNishSymbol(Symbol('foo'))).toStrictEqual(true); 250 | expect(isNishSymbol(null)).toStrictEqual(true); 251 | expect(isNishSymbol(undefined)).toStrictEqual(true); 252 | 253 | // Date 254 | const D1 = new Date(); 255 | expect(isDate(D1)).toStrictEqual(true); 256 | expect(isDate(false)).toStrictEqual(false); 257 | expect(isOptDate(D1)).toStrictEqual(true); 258 | expect(isOptDate(undefined)).toStrictEqual(true); 259 | expect(isNulDate(D1)).toStrictEqual(true); 260 | expect(isNulDate(null)).toStrictEqual(true); 261 | expect(isNishDate(D1)).toStrictEqual(true); 262 | expect(isNishDate(null)).toStrictEqual(true); 263 | expect(isNishDate(undefined)).toStrictEqual(true); 264 | 265 | // Valid Dates 266 | expect(isValidDate(1731195800809)).toStrictEqual(true); 267 | expect(isValidDate('2024-11-09T23:43:58.788Z')).toStrictEqual(true); 268 | expect(isValidDate('2024-111-09T23:43:58.788Z')).toStrictEqual(false); 269 | expect(isValidDate(12341234123412342)).toStrictEqual(false); 270 | 271 | // Date Arrays 272 | const D2 = new Date(), D3 = new Date(); 273 | expect(isDateArr([D1, D2, D3])).toStrictEqual(true); 274 | expect(isDateArr([D1, D2, '2024-10-30T20:08:36.838Z'])).toStrictEqual(false); 275 | expect(isDateArr(D1)).toStrictEqual(false); 276 | expect(isOptDateArr([D1, D2, D3])).toStrictEqual(true); 277 | expect(isOptDateArr(undefined)).toStrictEqual(true); 278 | expect(isNulDateArr([D1, D2, D3])).toStrictEqual(true); 279 | expect(isNulDateArr(null)).toStrictEqual(true); 280 | expect(isNishDateArr([D1, D2, D3])).toStrictEqual(true); 281 | expect(isNishDateArr(null)).toStrictEqual(true); 282 | expect(isNishDateArr(undefined)).toStrictEqual(true); 283 | 284 | // Obj 285 | const O1 = { val: 1 }; 286 | expect(isObj(O1)).toStrictEqual(true); 287 | expect(isObj(false)).toStrictEqual(false); 288 | expect(isOptObj(O1)).toStrictEqual(true); 289 | expect(isOptObj(undefined)).toStrictEqual(true); 290 | expect(isNulObj(O1)).toStrictEqual(true); 291 | expect(isNulObj(null)).toStrictEqual(true); 292 | expect(isNishObj(O1)).toStrictEqual(true); 293 | expect(isNishObj(null)).toStrictEqual(true); 294 | expect(isNishObj(undefined)).toStrictEqual(true); 295 | 296 | // Obj Arrays 297 | const O2 = { val: 2 }, O3 = { val: 3 }; 298 | expect(isObjArr([O1, O2, O3])).toStrictEqual(true); 299 | expect(isObjArr([O1, O2, '2024-10-30T20:08:36.838Z'])).toStrictEqual(false); 300 | expect(isObjArr(O1)).toStrictEqual(false); 301 | expect(isOptObjArr([O1, O2, O3])).toStrictEqual(true); 302 | expect(isOptObjArr(undefined)).toStrictEqual(true); 303 | expect(isNulObjArr([O1, O2, O3])).toStrictEqual(true); 304 | expect(isNulObjArr(null)).toStrictEqual(true); 305 | expect(isNishObjArr([O1, O2, O3])).toStrictEqual(true); 306 | expect(isNishObjArr(null)).toStrictEqual(true); 307 | expect(isNishObjArr(undefined)).toStrictEqual(true); 308 | 309 | // Functions 310 | const F1 = () => 1; 311 | expect(isFunc(F1)).toStrictEqual(true); 312 | expect(isFunc(false)).toStrictEqual(false); 313 | expect(isOptFunc(F1)).toStrictEqual(true); 314 | expect(isOptFunc(undefined)).toStrictEqual(true); 315 | expect(isNulFunc(F1)).toStrictEqual(true); 316 | expect(isNulFunc(null)).toStrictEqual(true); 317 | expect(isNishFunc(F1)).toStrictEqual(true); 318 | expect(isNishFunc(null)).toStrictEqual(true); 319 | expect(isNishFunc(undefined)).toStrictEqual(true); 320 | 321 | // Function Arrays 322 | const F2 = () => 2, F3 = () => 3; 323 | expect(isFuncArr([F1, F2, F3])).toStrictEqual(true); 324 | expect(isFuncArr([F1, F2, '2024-10-30T20:08:36.838Z'])).toStrictEqual(false); 325 | expect(isFuncArr(F1)).toStrictEqual(false); 326 | expect(isOptFuncArr([F1, F2, F3])).toStrictEqual(true); 327 | expect(isOptFuncArr(undefined)).toStrictEqual(true); 328 | expect(isNulFuncArr([F1, F2, F3])).toStrictEqual(true); 329 | expect(isNulFuncArr(null)).toStrictEqual(true); 330 | expect(isNishFuncArr([F1, F2, F3])).toStrictEqual(true); 331 | expect(isNishFuncArr(null)).toStrictEqual(true); 332 | expect(isNishFuncArr(undefined)).toStrictEqual(true); 333 | 334 | // Color 335 | expect(isColor('#ffffff')).toStrictEqual(true); 336 | expect(isColor('asdf')).toStrictEqual(false); 337 | expect(isOptColor('#ffffff')).toStrictEqual(true); 338 | expect(isOptColor(undefined)).toStrictEqual(true); 339 | expect(isNulColor('#ffffff')).toStrictEqual(true); 340 | expect(isNulColor(null)).toStrictEqual(true); 341 | expect(isOptColor('#ffffff')).toStrictEqual(true); 342 | expect(isNishColor(undefined)).toStrictEqual(true); 343 | expect(isNishColor(null)).toStrictEqual(true); 344 | expect(isNishColor(undefined)).toStrictEqual(true); 345 | 346 | // Email 347 | expect(isEmail('a@a.com')).toStrictEqual(true); 348 | expect(isEmail('asdf')).toStrictEqual(false); 349 | expect(isOptEmail('a@a.com')).toStrictEqual(true); 350 | expect(isOptEmail(undefined)).toStrictEqual(true); 351 | expect(isNulEmail('a@a.com')).toStrictEqual(true); 352 | expect(isNulEmail(null)).toStrictEqual(true); 353 | expect(isOptEmail('a@a.com')).toStrictEqual(true); 354 | expect(isNishEmail(undefined)).toStrictEqual(true); 355 | expect(isNishEmail(null)).toStrictEqual(true); 356 | expect(isNishEmail(undefined)).toStrictEqual(true); 357 | 358 | // Is Alpha-Numeric String 359 | expect(isAlphaNumStr('asdf1234')).toStrictEqual(true); 360 | expect(isAlphaNumStr('#ffffff')).toStrictEqual(false); 361 | expect(isOptAlphaNumStr('asdf1234')).toStrictEqual(true); 362 | expect(isOptAlphaNumStr(undefined)).toStrictEqual(true); 363 | expect(isNulAlphaNumStr('asdf1234')).toStrictEqual(true); 364 | expect(isNulAlphaNumStr(null)).toStrictEqual(true); 365 | expect(isOptAlphaNumStr('asdf1234')).toStrictEqual(true); 366 | expect(isNishAlphaNumStr(undefined)).toStrictEqual(true); 367 | expect(isNishAlphaNumStr(null)).toStrictEqual(true); 368 | expect(isNishAlphaNumStr(undefined)).toStrictEqual(true); 369 | 370 | // This will make the type '1' | '2' | '3' instead of just string[] 371 | const arr = ['1', '2', '3'] as const; 372 | // const check = isOptOrInArr(arr) 373 | 374 | // Is in Array 375 | expect(isInArr(arr)('1')).toStrictEqual(true); 376 | expect(isInArr(arr)(1)).toStrictEqual(false); 377 | expect(isOptOrInArr(arr)('1')).toStrictEqual(true); 378 | expect(isOptOrInArr(arr)(undefined)).toStrictEqual(true); 379 | expect(isNishOrInArr(arr)(undefined)).toStrictEqual(true); 380 | 381 | enum Scopes { 382 | Public = 'public', 383 | Private = 'private', 384 | } 385 | 386 | enum ScopesAlt { 387 | Public, 388 | Private, 389 | } 390 | 391 | // Enums 392 | expect(isEnumVal(Scopes)('public')).toStrictEqual(true); 393 | expect(isEnumVal(ScopesAlt)(1)).toStrictEqual(true); 394 | expect(isEnumVal(ScopesAlt)('private')).toStrictEqual(false); 395 | expect(isEnum(Scopes)).toStrictEqual(true); 396 | expect(isEnum(ScopesAlt)).toStrictEqual(true); 397 | expect(isOptEnumVal(ScopesAlt)(undefined)).toStrictEqual(true); 398 | expect(isNulEnumVal(ScopesAlt)(null)).toStrictEqual(true); 399 | expect(isNishEnumVal(ScopesAlt)(null)).toStrictEqual(true); 400 | expect(isNishEnumVal(ScopesAlt)(1)).toStrictEqual(true); 401 | 402 | // Non-Nullable 403 | expect(nonNullable(isNulStr)('asdf')).toStrictEqual(true); 404 | expect(nonNullable(isNulStr)(null)).toStrictEqual(false); 405 | expect(nonNullable(isNulStr)(undefined)).toStrictEqual(false); 406 | 407 | // Is Basic Object 408 | expect(isBasicObj({ a: 1, b: 2, c: 3 })).toStrictEqual(true); 409 | expect(isBasicObj([1, 2, 3])).toStrictEqual(false); 410 | 411 | // Check Object entries GOOD 412 | const isStrNumObj = iterateObjEntries>((key, val) => 413 | isStr(key) && isNum(val)); 414 | expect(isStrNumObj({ a: 1, b: 2, c: 3 })).toStrictEqual(true); 415 | expect(isStrNumObj({ a: 1, b: 2, c: 'asdf'})).toStrictEqual(false); 416 | 417 | // Check transform function 418 | const isNumArrWithParse = transform(safeJsonParse, isNumArr); 419 | expect(isNumArrWithParse('[1,2,3]', val => { 420 | expect(isNumArr(val)).toStrictEqual(true); 421 | })).toStrictEqual(true); 422 | 423 | // Check is key of Object 424 | const someObject = { 425 | foo: 'bar', 426 | bada: 'bing', 427 | } as const; 428 | 429 | const isKeyofSomeObject = isKeyOf(someObject); 430 | expect(isKeyofSomeObject('foo')).toStrictEqual(true); 431 | expect(isKeyofSomeObject('bada')).toStrictEqual(true); 432 | expect(isKeyofSomeObject('bing')).toStrictEqual(false); 433 | const isKeyofSomeObjectArr = isNulKeyOfArr(someObject); 434 | expect(isKeyofSomeObjectArr(['bada', 'foo'])).toStrictEqual(true); 435 | expect(isKeyofSomeObjectArr(null)).toStrictEqual(true); 436 | expect(isKeyofSomeObjectArr(['bar', 'foo', 'bing'])).toStrictEqual(false); 437 | }); 438 | 439 | 440 | /** 441 | * Test "parseObj()" function 442 | */ 443 | test('test "parseObj" function', () => { 444 | 445 | // ** Basic Test ** 446 | const parseUser = parseObj({ 447 | id: transform(Number, isNum), 448 | name: isStr, 449 | }); 450 | const user = parseUser({ 451 | id: '5', 452 | name: 'john', 453 | email: '--', 454 | }); 455 | expect(user).toStrictEqual({ id: 5, name: 'john' }); 456 | 457 | // ** Parse optional object ** // 458 | const parseOptUser = parseOptObj({ 459 | id: isNum, 460 | name: isStr, 461 | }); 462 | const optUser = parseOptUser({ 463 | id: 15, 464 | name: 'joe', 465 | email: '--', 466 | }); 467 | const optUser2 = parseOptUser(undefined); 468 | expect(optUser).toStrictEqual({ id: 15, name: 'joe' }); 469 | expect(optUser2).toStrictEqual(undefined); 470 | 471 | // ** Array Test ** // 472 | const userArr = [user, { id: 1, name: 'a' }, { id: 2, name: 'b' }], 473 | userArrBad = [user, { id: 1, name: 'a' }, { idd: 2, name: 'b' }]; 474 | // Normal array test 475 | const parseUserArr = parseObjArr({ 476 | id: isNum, 477 | name: isStr, 478 | }); 479 | const parsedUserArr = parseUserArr(userArr), 480 | parsedUserArrBad = parseOptUser(userArrBad); 481 | expect(userArr).toStrictEqual(parsedUserArr); 482 | expect(parsedUserArrBad).toStrictEqual(undefined); 483 | // Nullish or array 484 | const parseNishUserArr = parseNishObjArr({ 485 | id: isNum, 486 | name: isStr, 487 | }); 488 | const parsedNishUserArr = parseNishUserArr(null); 489 | expect(parsedNishUserArr).toStrictEqual(null); 490 | const parsedNishUserArr2 = parseNishUserArr(userArr); 491 | expect(parsedNishUserArr2).toStrictEqual(userArr); 492 | 493 | // ** Nested Object Test (Good) ** // 494 | const parseUserWithAddr = parseObj({ 495 | id: isNum, 496 | name: isStr, 497 | address: { 498 | city: isStr, 499 | zip: isNum, 500 | }, 501 | }); 502 | const userWithAddr = parseUserWithAddr({ 503 | id: 5, 504 | name: 'john', 505 | address: { 506 | city: 'seattle', 507 | zip: 98111, 508 | }, 509 | }); 510 | expect(userWithAddr).toStrictEqual({ 511 | id: 5, 512 | name: 'john', 513 | address: { 514 | city: 'seattle', 515 | zip: 98111, 516 | }, 517 | }); 518 | expect(userWithAddr.address.zip).toBe(98111); 519 | 520 | // ** Nested Object Test (Bad) ** // 521 | const userWithAddrBad = parseUserWithAddr({ 522 | id: 5, 523 | name: 'john', 524 | address: { 525 | city: 'seattle', 526 | zip: '98111', 527 | }, 528 | }); 529 | expect(userWithAddrBad).toBe(undefined); 530 | 531 | // ** Test parse "onError" function ** // 532 | const parseUserWithError = parseObj({ 533 | id: isNum, 534 | name: isStr, 535 | }, (prop, value) => { 536 | expect(prop).toStrictEqual('id'); 537 | expect(value).toStrictEqual('5'); 538 | }); 539 | parseUserWithError({ 540 | id: '5', 541 | name: 'john', 542 | }); 543 | 544 | // ** Test parseObj "onError" function for array argument ** // 545 | const parseUserArrWithError = parseObjArr({ 546 | id: isNum, 547 | name: isStr, 548 | }, (prop, value, index) => { 549 | expect(prop).toStrictEqual('id'); 550 | expect(value).toStrictEqual('3'); 551 | expect(index).toStrictEqual(2); 552 | }); 553 | parseUserArrWithError([ 554 | { id: 1, name: '1' }, 555 | { id: 2, name: '2' }, 556 | { id: '3', name: '3' }, 557 | { id: 3, name: '3' }, 558 | ]); 559 | 560 | // ** Test "parseObj" when validator throws error ** // 561 | const isStrWithErr = (val: unknown): val is string => { 562 | if (isStr(val)) { 563 | return true; 564 | } else { 565 | throw new Error('Value was not a valid string.'); 566 | } 567 | }; 568 | const parseUserHandleErr = parseObj({ 569 | id: isNum, 570 | name: isStrWithErr, 571 | }, (prop, value, caughtErr) => { 572 | expect(prop).toStrictEqual('name'); 573 | expect(value).toStrictEqual(null); 574 | expect(caughtErr).toStrictEqual('Value was not a valid string.'); 575 | }); 576 | parseUserHandleErr({ 577 | id: 5, 578 | name: null, 579 | }); 580 | }); 581 | 582 | 583 | /** 584 | * Test "testObj" function 585 | */ 586 | test('test "testObj" function', () => { 587 | 588 | // Do basic test 589 | const testUser = testObj({ 590 | id: isNum, 591 | name: isStr, 592 | address: { 593 | city: isStr, 594 | zip: transform(Number, isNum), 595 | }, 596 | }); 597 | const result = testUser({ 598 | id: 5, 599 | name: 'john', 600 | address: { 601 | city: 'Seattle', 602 | zip: '98109', 603 | }, 604 | }); 605 | expect(result).toStrictEqual(true); 606 | 607 | // Test combination of "parseObj" and "testObj" 608 | const testCombo = parseObj({ 609 | id: isNum, 610 | name: isStr, 611 | address: testObj({ 612 | city: isStr, 613 | zip: transform(Number, isNum), 614 | }), 615 | }); 616 | const user = testCombo({ 617 | id: 5, 618 | name: 'john', 619 | address: { 620 | city: 'Seattle', 621 | zip: '98109', 622 | }, 623 | }); 624 | expect(user).toStrictEqual({ 625 | id: 5, 626 | name: 'john', 627 | address: { 628 | city: 'Seattle', 629 | zip: 98109, 630 | }, 631 | }); 632 | }); 633 | -------------------------------------------------------------------------------- /src/validators.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | 3 | // **** Variables **** // 4 | 5 | const EMAIL_RGX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/, 6 | COLOR_RGX = new RegExp(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/), 7 | ALPHA_NUMERIC = new RegExp('^[a-zA-Z0-9]*$'), 8 | URL_RGX = /^((https?|ftp|smtp):\/\/)?(www.)?[a-z0-9]+(\.[a-z]{2,}){1,3}(#?\/?[a-zA-Z0-9#]+)*\/?(\?[a-zA-Z0-9-_]+=[a-zA-Z0-9-%]+&?)?$/; 9 | 10 | 11 | // **** Types **** // 12 | 13 | export type TEnum = Record; 14 | export type TEmail = `${string}@${string}`; 15 | export type TColor = `#${string}`; 16 | export type TURL = `${string}`; 17 | export type TBasicObj = Record; 18 | type TValidateWithTransform = (arg: unknown, cb?: (arg: T) => void) => arg is T; 19 | 20 | // Add modifiers 21 | type AddNull = (N extends true ? T | null : T); 22 | type AddNullables = (O extends true ? AddNull | undefined : AddNull); 23 | export type AddMods = A extends true ? AddNullables : AddNullables; 24 | 25 | 26 | // **** Functions **** // 27 | 28 | // Nullables 29 | export const isUndef = ((arg: unknown): arg is undefined => arg === undefined); 30 | export const isNull = ((arg: unknown): arg is null => arg === null); 31 | export const isNoU = _orNul(isUndef); 32 | 33 | // Boolean 34 | export const isBool = _checkType('boolean'); 35 | export const isOptBool = _orOpt(isBool); 36 | export const isNulBool = _orNul(isBool); 37 | export const isNishBool = _orNul(isOptBool); 38 | export const isBoolArr = _isArr(isBool); 39 | export const isOptBoolArr = _orOpt(isBoolArr); 40 | export const isNulBoolArr = _orNul(isBoolArr); 41 | export const isNishBoolArr = _orNul(isOptBoolArr); 42 | 43 | // Is valid boolean ("true"/"false", true/false, "1/0", "1"/"0", "yes"/"no") 44 | export const isValidBool = _transform(_parseBool, isBool); 45 | export const isOptValidBool = _orOpt(isValidBool); 46 | export const isNulValidBool = _orNul(isValidBool); 47 | export const isNishValidBool = _orNul(isOptValidBool); 48 | export const isValidBoolArr = _isArr(isValidBool); 49 | export const isOptValidBoolArr = _orOpt(isValidBoolArr); 50 | export const isNulValidBoolArr = _orNul(isValidBoolArr); 51 | export const isNishValidBoolArr = _orNul(isOptValidBoolArr); 52 | 53 | // Number 54 | export const isNum = _checkType('number'); 55 | export const isOptNum = _orOpt(isNum); 56 | export const isNulNum = _orNul(isNum); 57 | export const isNishNum = _orNul(isOptNum); 58 | export const isNumArr = _isArr(isNum); 59 | export const isOptNumArr = _orOpt(isNumArr); 60 | export const isNulNumArr = _orNul(isNumArr); 61 | export const isNishNumArr = _orNul(isOptNumArr); 62 | 63 | // BigInt 64 | export const isBigInt = _checkType('bigint'); 65 | export const isOptBigInt = _orOpt(isBigInt); 66 | export const isNulBigInt = _orNul(isBigInt); 67 | export const isNishBigInt = _orNul(isOptBigInt); 68 | export const isBigIntArr = _isArr(isBigInt); 69 | export const isOptBigIntArr = _orOpt(isBigIntArr); 70 | export const isNulBigIntArr = _orNul(isBigIntArr); 71 | export const isNishBigIntArr = _orNul(isOptBigIntArr); 72 | 73 | // Valid number (is it still a number after doing "Number(arg)", could be a string) 74 | export const isValidNum = _transform(Number, isNum); 75 | export const isOptValidNum = _orOpt(isValidNum); 76 | export const isNulValidNum = _orNul(isValidNum); 77 | export const isNishValidNum = _orNul(isOptValidNum); 78 | export const isValidNumArr = _isArr(isValidNum); 79 | export const isOptValidNumArr = _orOpt(isValidNumArr); 80 | export const isNulValidNumArr = _orNul(isValidNumArr); 81 | export const isNishValidNumArr = _orNul(isOptValidNumArr); 82 | 83 | // Range 84 | export const isInRange = _isInRange(false, false, false); 85 | export const isOptInRange = _isInRange(true, false, false); 86 | export const isNulInRange = _isInRange(false, true, false); 87 | export const isNishInRange = _isInRange(true, true, false); 88 | export const isInRangeArr = _isInRange(false, false, true); 89 | export const isOptInRangeArr = _isInRange(true, false, true); 90 | export const isNulInRangeArr = _isInRange(false, true, true); 91 | export const isNishInRangeArr = _isInRange(true, true, true); 92 | 93 | // String 94 | export const isStr = _checkType('string'); 95 | export const isOptStr = _orOpt(isStr); 96 | export const isNulStr = _orNul(isStr); 97 | export const isNishStr = _orNul(isOptStr); 98 | export const isStrArr = _isArr(isStr); 99 | export const isOptStrArr = _orOpt(isStrArr); 100 | export const isNulStrArr = _orNul(isStrArr); 101 | export const isNishStrArr = _orNul(isOptStrArr); 102 | 103 | // NeStr => "Non-Empty String" 104 | export const isNeStr = (arg: unknown): arg is string => (isStr(arg) && arg.length > 0); 105 | export const isOptNeStr = _orOpt(isNeStr); 106 | export const isNulNeStr = _orNul(isNeStr); 107 | export const isNishNeStr = _orNul(isOptNeStr); 108 | export const isNeStrArr = _isArr(isNeStr); 109 | export const isOptNeStrArr = _orOpt(isNeStrArr); 110 | export const isNulNeStrArr = _orNul(isNeStrArr); 111 | export const isNishNeStrArr = _orNul(isOptNeStrArr); 112 | 113 | // Symbol 114 | export const isSymbol = _checkType('symbol'); 115 | export const isOptSymbol = _orOpt(isSymbol); 116 | export const isNulSymbol = _orNul(isSymbol); 117 | export const isNishSymbol = _orNul(isOptSymbol); 118 | export const isSymbolArr = _isArr(isSymbol); 119 | export const isOptSymbolArr = _orOpt(isSymbolArr); 120 | export const isNulSymbolArr = _orNul(isSymbolArr); 121 | export const isNishSymbolArr = _orNul(isOptSymbolArr); 122 | 123 | // Date 124 | export const isDate = (arg: unknown): arg is Date => arg instanceof Date; 125 | export const isOptDate = _orOpt(isDate); 126 | export const isNulDate = _orNul(isDate); 127 | export const isNishDate = _orNul(isOptDate); 128 | export const isDateArr = _isArr(isDate); 129 | export const isOptDateArr = _orOpt(isDateArr); 130 | export const isNulDateArr = _orNul(isDateArr); 131 | export const isNishDateArr = _orNul(isOptDateArr); 132 | 133 | // Valid date (is it a valid date after calling "new Date()", could be a string or number) 134 | export const isValidDate = _transform((arg: unknown) => new Date(arg as Date), _isValidDate); 135 | export const isOptValidDate = _orOpt(isValidDate); 136 | export const isNulValidDate = _orNul(isValidDate); 137 | export const isNishValidDate = _orNul(isOptValidDate); 138 | export const isValidDateArr = _isArr(isValidDate); 139 | export const isOptValidDateArr = _orOpt(isValidDateArr); 140 | export const isNulValidDateArr = _orNul(isValidDateArr); 141 | export const isNishValidDateArr = _orNul(isOptValidDateArr); 142 | 143 | // Object 144 | export const isObj = _checkType('object'); 145 | export const isOptObj = _orOpt(isObj); 146 | export const isNulObj = _orNul(isObj); 147 | export const isNishObj = _orNul(isOptObj); 148 | export const isObjArr = _isArr(isObj); 149 | export const isOptObjArr = _orOpt(isObjArr); 150 | export const isNulObjArr = _orNul(isObjArr); 151 | export const isNishObjArr = _orNul(isOptObjArr); 152 | 153 | // Function 154 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 155 | export const isFunc = _checkType<(...args: any[]) => any>('function'); 156 | export const isOptFunc = _orOpt(isFunc); 157 | export const isNulFunc = _orNul(isFunc); 158 | export const isNishFunc = _orNul(isOptFunc); 159 | export const isFuncArr = _isArr(isFunc); 160 | export const isOptFuncArr = _orOpt(isFuncArr); 161 | export const isNulFuncArr = _orNul(isFuncArr); 162 | export const isNishFuncArr = _orNul(isOptFuncArr); 163 | 164 | // Color 165 | export const isColor = _isRgx(COLOR_RGX); 166 | export const isOptColor = _orOpt(isColor); 167 | export const isNulColor = _orNul(isColor); 168 | export const isNishColor = _orNul(isOptColor); 169 | 170 | // Email 171 | export const isEmail = _isRgx(EMAIL_RGX); 172 | export const isOptEmail = _orOpt(isEmail); 173 | export const isNulEmail = _orNul(isEmail); 174 | export const isNishEmail = _orNul(isOptEmail); 175 | 176 | // URL 177 | export const isUrl = _isRgx(URL_RGX); 178 | export const isOptUrl = _orOpt(isUrl); 179 | export const isNulUrl = _orNul(isUrl); 180 | export const isNishUrl = _orNul(isOptUrl); 181 | 182 | // Alpha-Numeric String 183 | export const isAlphaNumStr = _isRgx(ALPHA_NUMERIC); 184 | export const isOptAlphaNumStr = _orOpt(isAlphaNumStr); 185 | export const isNulAlphaNumStr = _orNul(isAlphaNumStr); 186 | export const isNishAlphaNumStr = _orNul(isOptAlphaNumStr); 187 | 188 | // Basic Objects 189 | export const isBasicObj = _isBasicObj; 190 | export const isOptBasicObj = _orOpt(isBasicObj); 191 | export const isNulBasicObj = _orNul(isBasicObj); 192 | export const isNishBasicObj = _orNul(isOptBasicObj); 193 | 194 | // Is in array 195 | export const isInArr = (arg: T) => _isInArr(arg, false, false); 196 | export const isOptOrInArr = (arg: T) => _isInArr(arg, true, false); 197 | export const isNulOrInArr = (arg: T) => _isInArr(arg, false, true); 198 | export const isNishOrInArr = (arg: T) => _isInArr(arg, true, true); 199 | 200 | // Enums (NOTE: this does not work for mixed enums see: eslint@typescript-eslint/no-mixed-enums) 201 | export const isEnum = _isEnum; 202 | export const isOptEnum = _orOpt(_isEnum); 203 | export const isNulEnum = _orNul(_isEnum); 204 | export const isNishEnum = _orNul(isOptEnum); 205 | 206 | // Is Enum value 207 | export const isEnumVal = (arg: T) => _isEnumVal(arg, false, false); 208 | export const isOptEnumVal = (arg: T) => _isEnumVal(arg, true, false); 209 | export const isNulEnumVal = (arg: T) => _isEnumVal(arg, false, true); 210 | export const isNishEnumVal = (arg: T) => _isEnumVal(arg, true, true); 211 | 212 | // Is Key of an Object 213 | export const isKeyOf = (arg: T) => _isKeyOf(arg, false, false, false); 214 | export const isOptKeyOf = (arg: T) => _isKeyOf(arg, true, false, false); 215 | export const isNulKeyOf = (arg: T) => _isKeyOf(arg, false, true, false); 216 | export const isNishKeyOf = (arg: T) => _isKeyOf(arg, true, true, false); 217 | export const isKeyOfArr = (arg: T) => _isKeyOf(arg, false, false, true); 218 | export const isOptKeyOfArr = (arg: T) => _isKeyOf(arg, true, false, true); 219 | export const isNulKeyOfArr = (arg: T) => _isKeyOf(arg, false, true, true); 220 | export const isNishKeyOfArr = (arg: T) => _isKeyOf(arg, true, true, true); 221 | 222 | // Parse Object (check the properties against a schema) 223 | export const parseObj = (arg: U, onError?: TParseOnError) => _parseObj(arg, false, false, false, onError); 224 | export const parseOptObj = (arg: U, onError?: TParseOnError) => _parseObj(arg, true, false, false, onError); 225 | export const parseNulObj = (arg: U, onError?: TParseOnError) => _parseObj(arg, false, true, false, onError); 226 | export const parseNishObj = (arg: U, onError?: TParseOnError) => _parseObj(arg, true, true, false, onError); 227 | export const parseObjArr = (arg: U, onError?: TParseOnError) => _parseObj(arg, false, false, true, onError); 228 | export const parseOptObjArr = (arg: U, onError?: TParseOnError) => _parseObj(arg, true, false, true, onError); 229 | export const parseNulObjArr = (arg: U, onError?: TParseOnError) => _parseObj(arg, false, true, true, onError); 230 | export const parseNishObjArr = (arg: U, onError?: TParseOnError) => _parseObj(arg, true, true, true, onError); 231 | 232 | // Test Object (like "parseObj" but returns a type predicate instead) 233 | export const testObj = (arg: U, onError?: TParseOnError) => _testObj(arg, false, false, false, onError); 234 | export const testOptObj = (arg: U, onError?: TParseOnError) => _testObj(arg, true, false, false, onError); 235 | export const testNulObj = (arg: U, onError?: TParseOnError) => _testObj(arg, false, true, false, onError); 236 | export const testNishObj = (arg: U, onError?: TParseOnError) => _testObj(arg, true, true, false, onError); 237 | export const testObjArr = (arg: U, onError?: TParseOnError) => _testObj(arg, false, false, true, onError); 238 | export const testOptObjArr = (arg: U, onError?: TParseOnError) => _testObj(arg, true, false, true, onError); 239 | export const testNulObjArr = (arg: U, onError?: TParseOnError) => _testObj(arg, false, true, true, onError); 240 | export const testNishObjArr = (arg: U, onError?: TParseOnError) => _testObj(arg, true, true, true, onError); 241 | 242 | // Util 243 | export const iterateObjEntries = _iterateObjEntries; 244 | export const nonNullable = _nonNullable; 245 | export const transform = _transform; 246 | export const parseBool = _parseBool; 247 | export const safeJsonParse = _safeJsonParse; 248 | 249 | 250 | // **** Helpers **** // 251 | 252 | /** 253 | * Extract null/undefined from a validator function. 254 | */ 255 | function _nonNullable(cb: ((arg: unknown) => arg is T)) { 256 | return (arg: unknown): arg is NonNullable => { 257 | if (isNoU(arg)) { 258 | return false; 259 | } else { 260 | return cb(arg); 261 | } 262 | }; 263 | } 264 | 265 | /** 266 | * Do a validator callback function for each object entry. 267 | */ 268 | function _iterateObjEntries>( 269 | cb: (key: string, val: unknown) => boolean, 270 | ): (arg: unknown) => arg is T { 271 | return (arg: unknown): arg is T => { 272 | if (isObj(arg)) { 273 | for (const entry of Object.entries(arg)) { 274 | if (!cb(entry[0], entry[1])) { 275 | return false; 276 | } 277 | } 278 | } 279 | return true; 280 | }; 281 | } 282 | 283 | /** 284 | * Allow param to be undefined 285 | */ 286 | function _orOpt(cb: ((arg: unknown) => arg is T)) { 287 | return (arg: unknown): arg is (T | undefined) => { 288 | if (isUndef(arg)) { 289 | return true; 290 | } else { 291 | return cb(arg); 292 | } 293 | }; 294 | } 295 | 296 | /** 297 | * Allow param to be undefined 298 | */ 299 | function _orNul(cb: ((arg: unknown) => arg is T)) { 300 | return (arg: unknown): arg is (T | null) => { 301 | if (arg === null) { 302 | return true; 303 | } else { 304 | return cb(arg); 305 | } 306 | }; 307 | } 308 | 309 | /** 310 | * Check array counterpart for validator item. 311 | */ 312 | function _isArr(cb: ((arg: unknown) => arg is T)) { 313 | return (arg: unknown): arg is T[] => { 314 | return Array.isArray(arg) && !arg.some(item => !cb(item)); 315 | }; 316 | } 317 | 318 | /** 319 | * See if a string satisfies the regex. NOTE: this lets an empty string be a 320 | * valid value. 321 | */ 322 | function _isRgx(rgx: RegExp) { 323 | return (arg: unknown): arg is T => { 324 | return (isStr(arg) && arg.length < 254 && (arg === '' || rgx.test(arg))); 325 | }; 326 | } 327 | 328 | /** 329 | * Wrapper to check basic type. 330 | */ 331 | function _checkType(type: string) { 332 | return (arg: unknown): arg is T => { 333 | return ( 334 | typeof arg === type && 335 | (type === 'object' ? (arg !== null) : true) && 336 | (type === 'number' ? !isNaN(arg as number) : true) 337 | ); 338 | }; 339 | } 340 | 341 | /** 342 | * Is an item in an array. 343 | */ 344 | export function _isInArr< 345 | T extends readonly unknown[], 346 | O extends boolean, 347 | N extends boolean 348 | >( 349 | arr: T, 350 | optional: O, 351 | nullable: N, 352 | ): (arg: unknown) => arg is AddNullables { 353 | return (arg: unknown): arg is AddNullables => { 354 | if (isUndef(arg)) { 355 | return !!optional; 356 | } 357 | if (isNull(arg)) { 358 | return !!nullable; 359 | } 360 | for (const item of arr) { 361 | if (arg === item) { 362 | return true; 363 | } 364 | } 365 | return false; 366 | }; 367 | } 368 | 369 | /** 370 | * Range will always determine if a number is >= the min and <= the max. If you want to 371 | * leave off a range, just use null. 372 | * 373 | * Examples: 374 | * isRange(0, null) => "0 or any positive number" 375 | * isRange(100, null) => "greater than or equal to 100" 376 | * isRange(25, 75) => "between 25 and 75" 377 | */ 378 | function _isInRange< 379 | O extends boolean, 380 | N extends boolean, 381 | A extends boolean, 382 | Ret = AddMods, 383 | >( 384 | optional: boolean, 385 | nullable: boolean, 386 | isArr: boolean, 387 | ): (min: number | null, max: number | null) => ((arg: unknown) => arg is Ret) { 388 | return (min: number | null, max: number | null): ((arg: unknown) => arg is Ret) => { 389 | return (arg: unknown): arg is Ret => { 390 | if (arg === undefined) { 391 | return optional; 392 | } 393 | if (arg === null) { 394 | return nullable; 395 | } 396 | if (isArr) { 397 | return Array.isArray(arg) && !arg.some(item => !_isInRangeCore(item, min, max)); 398 | } 399 | return _isInRangeCore(arg, min, max); 400 | }; 401 | }; 402 | } 403 | 404 | /** 405 | * Core logic for is array function. 406 | */ 407 | function _isInRangeCore(arg: unknown, min: number | null, max: number | null) { 408 | if (!isNum(arg)) { 409 | return false; 410 | } 411 | if (min !== null && arg < min) { 412 | return false; 413 | } 414 | if (max !== null && arg > max) { 415 | return false; 416 | } 417 | return true; 418 | } 419 | 420 | /** 421 | * See if something is a key of an object. 422 | */ 423 | function _isKeyOf< 424 | T extends Record, 425 | O extends boolean, 426 | N extends boolean, 427 | A extends boolean, 428 | Ret = AddMods, 429 | >( 430 | obj: Record, 431 | optional: boolean, 432 | nullable: boolean, 433 | isArr: boolean, 434 | ): ((arg: unknown) => arg is Ret) { 435 | if (!isBasicObj(obj)) { 436 | throw new Error('Item to check from must be a Record'); 437 | } 438 | const isInKeys = isInArr(Object.keys(obj)); 439 | return (arg: unknown): arg is Ret => { 440 | if (arg === undefined) { 441 | return optional; 442 | } 443 | if (arg === null) { 444 | return nullable; 445 | } 446 | if (isArr) { 447 | return Array.isArray(arg) && !arg.some(item => !isInKeys(item)); 448 | } 449 | return isInKeys(arg); 450 | }; 451 | } 452 | 453 | /** 454 | * Transform a value before checking it. 455 | */ 456 | function _transform( 457 | transFn: (arg: unknown) => T, 458 | vldt: ((arg: unknown) => arg is T), 459 | ): TValidateWithTransform { 460 | return (arg: unknown, cb?: (arg: T) => void): arg is T => { 461 | if (arg !== undefined) { 462 | arg = transFn(arg); 463 | } 464 | cb?.(arg as T); 465 | return vldt(arg); 466 | }; 467 | } 468 | 469 | /** 470 | * Is valid date. 471 | */ 472 | function _isValidDate(arg: unknown): arg is Date { 473 | return arg instanceof Date && !isNaN(arg.getTime()); 474 | } 475 | 476 | /** 477 | * Convert all string/number boolean types to a boolean. If not a valid boolean 478 | * return undefined. 479 | */ 480 | function _parseBool(arg: unknown): boolean | undefined { 481 | if (typeof arg === 'string') { 482 | arg = arg.toLowerCase(); 483 | if (arg === 'true') { 484 | return true; 485 | } else if (arg === 'false') { 486 | return false; 487 | } else if (arg === 'yes') { 488 | return true; 489 | } else if (arg === 'no') { 490 | return false; 491 | } else if (arg === '1') { 492 | return true; 493 | } else if (arg === '0') { 494 | return false; 495 | } 496 | } else if (typeof arg === 'number') { 497 | if (arg === 1) { 498 | return true; 499 | } else if (arg === 0) { 500 | return false; 501 | } 502 | } else if (typeof arg === 'boolean') { 503 | return arg; 504 | } 505 | // Default 506 | return undefined; 507 | } 508 | 509 | /** 510 | * Safe JSON parse 511 | */ 512 | function _safeJsonParse(arg: unknown): T { 513 | if (isStr(arg)) { 514 | return JSON.parse(arg) as T; 515 | } else { 516 | throw Error('JSON parse argument must be a string'); 517 | } 518 | } 519 | 520 | 521 | // **** Enum Stuff **** // 522 | 523 | const _isStrEnum = _iterateObjEntries((key, val) => (isStr(key) && isStr(val))); 524 | 525 | /** 526 | * Check if unknown is a valid enum object. 527 | * NOTE: this does not work for mixed enums see: "eslint@typescript-eslint/no-mixed-enums" 528 | */ 529 | function _isEnum(arg: unknown): arg is TEnum { 530 | // Check is non-array object 531 | if (!(isObj(arg) && !Array.isArray(arg))) { 532 | return false; 533 | } 534 | // Check if string or number enum 535 | const param = (arg as TBasicObj), 536 | keys = Object.keys(param), 537 | middle = Math.floor(keys.length / 2); 538 | // ** String Enum ** // 539 | if (!isNum(param[keys[middle]])) { 540 | return _isStrEnum(arg); 541 | } 542 | // ** Number Enum ** // 543 | // Enum key length will always be even 544 | if (keys.length % 2 !== 0) { 545 | return false; 546 | } 547 | // Check key/values 548 | for (let i = 0; i < middle; i++) { 549 | const thisKey = keys[i], 550 | thisVal = param[thisKey], 551 | thatKey = keys[i + middle], 552 | thatVal = param[thatKey]; 553 | if (!(thisVal === thatKey && thisKey === String(thatVal))) { 554 | return false; 555 | } 556 | } 557 | // Return 558 | return true; 559 | } 560 | 561 | /** 562 | * Check is value satisfies enum. 563 | */ 564 | function _isEnumVal( 568 | enumArg: T, 569 | optional: O, 570 | nullable: N, 571 | ): ((arg: unknown) => arg is AddNullables) { 572 | // Check is enum 573 | if (!_isEnum(enumArg)) { 574 | throw Error('Item to check from must be an enum.'); 575 | } 576 | // Get keys 577 | let resp = Object.keys(enumArg).reduce((arr: unknown[], key) => { 578 | if (!arr.includes(key)) { 579 | arr.push(enumArg[key]); 580 | } 581 | return arr; 582 | }, []); 583 | // Check if string or number enum 584 | if (isNum(enumArg[resp[0] as string])) { 585 | resp = resp.map(item => enumArg[item as string]); 586 | } 587 | // Return validator function 588 | return (arg: unknown): arg is AddNullables => { 589 | if (isUndef(arg)) { 590 | return !!optional; 591 | } 592 | if (isNull(arg)) { 593 | return !!nullable; 594 | } 595 | return resp.some(val => arg === val); 596 | }; 597 | } 598 | 599 | /** 600 | * Is the object Record. 601 | */ 602 | function _isBasicObj(arg: unknown): arg is TBasicObj { 603 | return (isObj(arg) && !Array.isArray(arg) && isStrArr(Object.keys(arg))); 604 | } 605 | 606 | 607 | // **** Parse/Test Object **** // 608 | 609 | interface TSchema { 610 | [key: string]: TValidateWithTransform | TSchema; 611 | } 612 | 613 | type TInferParseRes> = ( 614 | AddMods 615 | ); 616 | 617 | type TInferParseResHelper = { 618 | [K in keyof U]: ( 619 | U[K] extends TValidateWithTransform 620 | ? X 621 | : U[K] extends TSchema 622 | ? TInferParseResHelper 623 | : never 624 | ); 625 | }; 626 | 627 | type TParseOnError = ( 628 | A extends true 629 | ? ((property?: string, value?: unknown, index?: number, caughtErr?: unknown) => void) 630 | : ((property?: string, value?: unknown, caughtErr?: unknown) => void) 631 | ); 632 | 633 | /** 634 | * Validates an object schema, calls an error function is supplied one, returns 635 | * "undefined" if the parse fails, and works recursively too. NOTE: this will 636 | * purge all keys not part of the schema. 637 | */ 638 | function _parseObj< 639 | U extends TSchema, 640 | O extends boolean, 641 | N extends boolean, 642 | A extends boolean, 643 | >( 644 | schema: U, 645 | optional: O, 646 | nullable: N, 647 | isArr: A, 648 | onError?: TParseOnError, 649 | ) { 650 | return (arg: unknown) => _parseObjCore( 651 | !!optional, 652 | !!nullable, 653 | isArr, 654 | schema, 655 | arg, 656 | onError, 657 | ) as TInferParseRes; 658 | } 659 | 660 | /** 661 | * Validate the schema. 662 | */ 663 | function _parseObjCore( 664 | optional: boolean, 665 | nullable: boolean, 666 | isArr: A, 667 | schema: TSchema, 668 | arg: unknown, 669 | onError?: TParseOnError, 670 | ) { 671 | // Check "undefined" 672 | if (arg === undefined) { 673 | if (!optional) { 674 | onError?.('object value was undefined but not optional', arg); 675 | return undefined; 676 | } 677 | } 678 | // Check "null" 679 | if (arg === null) { 680 | if (!nullable) { 681 | onError?.('object value was null but not nullable', arg); 682 | return undefined; 683 | } 684 | return null; 685 | } 686 | // Check "array" 687 | if (isArr) { 688 | if (!Array.isArray(arg)) { 689 | onError?.('object not an array', arg); 690 | return null; 691 | } 692 | // Iterate array 693 | const resp = []; 694 | for (let i = 0; i < arg.length; i++) { 695 | const item: unknown = arg[i]; 696 | const parsedItem = _parseObjCoreHelper(schema, item, (prop, val, caughtErr) => { 697 | onError?.(prop, val, i, caughtErr); 698 | }); 699 | if (parsedItem === undefined) { 700 | return undefined; 701 | } else { 702 | resp.push(parsedItem); 703 | } 704 | } 705 | return resp; 706 | // Default 707 | } else { 708 | return _parseObjCoreHelper(schema, arg, onError as TParseOnError); 709 | } 710 | } 711 | 712 | /** 713 | * Iterate an object, apply a validator function to to each property in an 714 | * object using the schema. 715 | */ 716 | function _parseObjCoreHelper( 717 | schema: TSchema, 718 | arg: unknown, 719 | onError?: TParseOnError, 720 | ): unknown { 721 | if (!isObj(arg)) { 722 | return; 723 | } 724 | const retVal = (arg as TBasicObj); 725 | for (const key in schema) { 726 | const schemaProp = schema[key], 727 | val = retVal[key]; 728 | // Nested object 729 | if (typeof schemaProp === 'object') { 730 | const childVal = _parseObjCoreHelper(schemaProp, val, onError); 731 | if (childVal === undefined) { 732 | return undefined; 733 | } 734 | // Run validator 735 | } else if (typeof schemaProp === 'function') { 736 | try { 737 | if (!schemaProp(val, (tval: unknown) => { 738 | retVal[key] = tval; 739 | })) { 740 | return onError?.(key, val); 741 | } 742 | } catch (err) { 743 | if (err instanceof Error) { 744 | return onError?.(key, val, err.message); 745 | } else { 746 | return onError?.(key, val, err); 747 | } 748 | } 749 | } 750 | } 751 | // Purse keys not in schema 752 | for (const key in retVal) { 753 | if (!(key in schema)) { 754 | Reflect.deleteProperty(arg, key); 755 | } 756 | } 757 | // Return 758 | return retVal; 759 | } 760 | 761 | /** 762 | * Like "parseObj" but returns a type-predicate instead of the object. 763 | */ 764 | function _testObj< 765 | U extends TSchema, 766 | O extends boolean, 767 | N extends boolean, 768 | A extends boolean, 769 | >( 770 | schema: U, 771 | optional: O, 772 | nullable: N, 773 | isArr: A, 774 | onError?: TParseOnError, 775 | ) { 776 | const parseFn = _parseObj(schema, optional, nullable, isArr, onError); 777 | return (arg: unknown): arg is typeof objRes => { 778 | const objRes = parseFn(arg); 779 | if (objRes === undefined) { 780 | return false; 781 | } else { 782 | return true; 783 | } 784 | }; 785 | } 786 | --------------------------------------------------------------------------------