Repository: 5alidz/tiny-schema-validator Branch: master Commit: e7b8dd074178 Files: 21 Total size: 58.9 KB Directory structure: gitextract_l7zhuy_5/ ├── .eslintignore ├── .eslintrc.js ├── .github/ │ └── workflows/ │ ├── main.yml │ └── size.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src/ │ ├── constants.ts │ ├── createErrors.ts │ ├── createSchema.ts │ ├── helpers.ts │ ├── index.ts │ ├── type-utils.ts │ ├── utils.ts │ └── validatorTypes.ts ├── test/ │ ├── index.test.ts │ ├── tsconfig.json │ └── validators.test.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ dist/ ================================================ FILE: .eslintrc.js ================================================ module.exports = { rules: { eqeqeq: 'off', 'no-redeclare': 'off', '@typescript-eslint/no-redeclare': ['error', { ignoreDeclarationMerge: true }], }, }; ================================================ FILE: .github/workflows/main.yml ================================================ name: CI on: [push] jobs: build: name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: node: ['10.x', '12.x', '14.x'] os: [ubuntu-latest, windows-latest, macOS-latest] steps: - name: Checkout repo uses: actions/checkout@v2 - name: Use Node ${{ matrix.node }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node }} - name: Install deps and build (with cache) uses: bahmutov/npm-install@v1 - name: Lint run: yarn lint - name: Test run: yarn test --ci --coverage --maxWorkers=2 - name: Build run: yarn build ================================================ FILE: .github/workflows/size.yml ================================================ name: size on: [pull_request] jobs: size: runs-on: ubuntu-latest env: CI_JOB_NUMBER: 1 steps: - uses: actions/checkout@v1 - uses: andresz1/size-limit-action@v1 with: github_token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ *.log .DS_Store node_modules dist coverage todo ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. ### [5.0.3](https://github.com/5alidz/tiny-schema-validator/compare/v5.0.2...v5.0.3) (2021-08-31) ### Bug Fixes * remove unused import ([8c1247a](https://github.com/5alidz/tiny-schema-validator/commit/8c1247a5be26a40209e584c7ea856db85a9896fd)) * report unknown keys found in data on validation ([#2](https://github.com/5alidz/tiny-schema-validator/issues/2)) ([08a4e5c](https://github.com/5alidz/tiny-schema-validator/commit/08a4e5cda8a01710d8964db9ab199fc94d6e23e1)) * test and fix unknown keys ([aff85e1](https://github.com/5alidz/tiny-schema-validator/commit/aff85e1d3177a2db7a9c1c5947820db0a3521329)) ### [5.0.2](https://github.com/5alidz/tiny-schema-validator/compare/v5.0.1...v5.0.2) (2021-08-28) ### [5.0.1](https://github.com/5alidz/tiny-schema-validator/compare/v5.0.0...v5.0.1) (2021-08-28) ## [5.0.0](https://github.com/5alidz/tiny-schema-validator/compare/v4.0.0...v5.0.0) (2021-08-23) ### ⚠ BREAKING CHANGES * - replace "traverse" with "source" so you can parse the schema however you want, also to add "atomic" validations * remove traverse, and replace it with direct validation ([513c682](https://github.com/5alidz/tiny-schema-validator/commit/513c682acd3d1845acbc73c44f52ed766e5039a5)) ## [4.0.0](https://github.com/5alidz/tiny-schema-validator/compare/v3.1.0...v4.0.0) (2021-08-21) ### ⚠ BREAKING CHANGES * when traversing the schema and the returned value is null, the result will contain 'invalid-type' message ### Features * add constant and union validators ([7859392](https://github.com/5alidz/tiny-schema-validator/commit/7859392dffdda3bc7adeaa6fc5f6df6085b2d5a1)) * include the schema source on the schema api ([14c1ecf](https://github.com/5alidz/tiny-schema-validator/commit/14c1ecf2e8bf0e7e9429bb34c6c58e88199bb4c2)) ## [3.1.0](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.5...v3.1.0) (2021-07-18) ### Features * add latest version of typescript ([2e8a339](https://github.com/5alidz/tiny-schema-validator/commit/2e8a339d392bb229d52f79ded5f563e1953e2dc6)) ### [3.0.5](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.4...v3.0.5) (2021-04-05) ### Bug Fixes * narrow down types in case of {} ([5cf59cf](https://github.com/5alidz/tiny-schema-validator/commit/5cf59cfaf5b6b851e0adbce055dd6fb364ccc8c6)) ### [3.0.4](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.3...v3.0.4) (2021-04-05) ### Bug Fixes * fix shape validators recursion even when optional ([994cef6](https://github.com/5alidz/tiny-schema-validator/commit/994cef6229b2accc49c46972e94ef5b41fe8d275)) * move data checking outside the loop ([2d40ec1](https://github.com/5alidz/tiny-schema-validator/commit/2d40ec1a10633d5a20c738e1f44f2da9acbcd7a9)) * optimize types for strict traverse ([4fd3d26](https://github.com/5alidz/tiny-schema-validator/commit/4fd3d26f7e6380a2d9a23ffcbb9804a1d166075b)) * throw TypeError on produce invalid-data ([0462755](https://github.com/5alidz/tiny-schema-validator/commit/04627558e7007ac39622aa085b3380f54997a750)) ### [3.0.3](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.1...v3.0.3) (2021-04-02) ### Bug Fixes * **types:** fix infered schema.embed ([ade04e0](https://github.com/5alidz/tiny-schema-validator/commit/ade04e0684dd0d4cdd1887a32c74ce6542913e90)) ### [3.0.2](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.1...v3.0.2) (2021-04-02) ### Bug Fixes * **types:** fix infered schema.embed ([ade04e0](https://github.com/5alidz/tiny-schema-validator/commit/ade04e0684dd0d4cdd1887a32c74ce6542913e90)) ### [3.0.1](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.0...v3.0.1) (2021-03-28) ## [3.0.0](https://github.com/5alidz/tiny-schema-validator/compare/v3.0.0-alpha.0...v3.0.0) (2021-03-26) ### Bug Fixes * helper better type support & insure primitives is required ([ab37494](https://github.com/5alidz/tiny-schema-validator/commit/ab374945dd1b439701161f38b183b7c1d4fecd7a)) ## [3.0.0-alpha.0](https://github.com/5alidz/tiny-schema-validator/compare/v2.1.0-alpha.3...v3.0.0-alpha.0) (2021-03-26) ### ⚠ BREAKING CHANGES * infers data type automatically for both JS & TS ### Features * implement better type inference | less work for the user ([a827591](https://github.com/5alidz/tiny-schema-validator/commit/a827591a8ce525b8f32d08e99ffdb8f8f9657485)) ### Bug Fixes * fix validator circular refernce ([f922048](https://github.com/5alidz/tiny-schema-validator/commit/f922048af6faca4389e7d0abfd5c35097946e916)) ## [2.1.0-alpha.3](https://github.com/5alidz/tiny-schema-validator/compare/v2.1.0-alpha.2...v2.1.0-alpha.3) (2021-03-19) ## [2.1.0-alpha.2](https://github.com/5alidz/tiny-schema-validator/compare/v2.1.0-alpha.1...v2.1.0-alpha.2) (2021-03-18) ### Bug Fixes * expose validatorTypes with index.d.ts ([796990d](https://github.com/5alidz/tiny-schema-validator/commit/796990d543de176332973ef198b33e5d8a48ea1d)) ## [2.1.0-alpha.1](https://github.com/5alidz/tiny-schema-validator/compare/v2.1.0-alpha.0...v2.1.0-alpha.1) (2021-03-11) ### Bug Fixes * expose path as array of strings ([f304909](https://github.com/5alidz/tiny-schema-validator/commit/f304909c9d06bf118cf9d33bc0bfa2043f8ff424)) * remove repeated parent path ([ecd7efa](https://github.com/5alidz/tiny-schema-validator/commit/ecd7efa427156c5e56c5a225975451bf467699cc)) ## [2.1.0-alpha.0](https://github.com/5alidz/tiny-schema-validator/compare/v2.0.1-alpha.3...v2.1.0-alpha.0) (2021-03-10) ### Features * expose correct path ([2fa69ae](https://github.com/5alidz/tiny-schema-validator/commit/2fa69ae08c6c95ee76afe60c07da1c060a726208)) ### [2.0.1-alpha.3](https://github.com/5alidz/tiny-schema-validator/compare/v2.0.1-alpha.2...v2.0.1-alpha.3) (2021-03-10) ### Bug Fixes * expose parentkey ([d6d4302](https://github.com/5alidz/tiny-schema-validator/commit/d6d43028f858983b4f74cfeb2908693c56465ded)) ### [2.0.1-alpha.2](https://github.com/5alidz/tiny-schema-validator/compare/v2.0.1-alpha.1...v2.0.1-alpha.2) (2021-03-10) ### Bug Fixes * prettier not supporting export * as ([0bc2a29](https://github.com/5alidz/tiny-schema-validator/commit/0bc2a2960cdee7c135a1fd57245c1892e2b7293d)) * recordof traverse using index instead of key ([9606738](https://github.com/5alidz/tiny-schema-validator/commit/96067381a3115d087f2633dfa7291d238eb01243)) ### [2.0.1-alpha.1](https://github.com/5alidz/tiny-schema-validator/compare/v2.0.1-alpha.0...v2.0.1-alpha.1) (2021-03-06) ### Bug Fixes * better type inference ([a233541](https://github.com/5alidz/tiny-schema-validator/commit/a233541bd1337fc289427046bac02d5b804e15a8)) ### [2.0.1-alpha.0](https://github.com/5alidz/tiny-schema-validator/compare/v2.0.0...v2.0.1-alpha.0) (2021-03-06) ### Bug Fixes * infer errors types ([6ea7d1f](https://github.com/5alidz/tiny-schema-validator/commit/6ea7d1f9ac62aabff78e627d1133c947f10e0d95)) ## [2.0.0](https://github.com/5alidz/tiny-schema-validator/compare/v1.0.5...v2.0.0) (2021-03-04) ### ⚠ BREAKING CHANGES * renamed recordOf -> recordof * change helpers names to match docs ([65a1f29](https://github.com/5alidz/tiny-schema-validator/commit/65a1f298323d397d7399933252b2022bdacc784a)) ### [1.0.5](https://github.com/5alidz/tiny-schema-validator/compare/v1.0.4...v1.0.5) (2021-03-02) ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 khaled 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. ================================================ FILE: README.md ================================================ # Tiny Schema Validator JSON schema validator with excellent type inference for JavaScript and TypeScript. [![GitHub license](https://img.shields.io/github/license/5alidz/tiny-schema-validator)](https://github.com/5alidz/tiny-schema-validator/blob/master/LICENSE) ![Minzipped size](https://img.shields.io/bundlephobia/minzip/tiny-schema-validator.svg) ## Installation ```sh npm install tiny-schema-validator # or yarn add tiny-schema-validator ``` ## Usage ### Creating a schema ```js import { createSchema, _ } from 'tiny-schema-validator'; export const User = createSchema({ metadata: _.record({ date_created: _.number(), id: _.string(), }), profile: _.record({ name: _.string({ maxLength: [100, 'too-long'], minLength: [2, 'too-short'], }), age: _.number({ max: [150, 'too-old'], min: [13, 'too-young'], }), email: _.string({ pattern: [/^[^@]+@[^@]+\.[^@]+$/, 'invalid-email'], }), }), payment_status: _.union( _.constant('pending'), _.constant('failed'), _.constant('success'), _.constant('canceled') ), }); ``` and in TypeScript, everything is the same, but to get the data type inferred from the schema, you can do this: ```ts /* UserType { metadata: { date_created: number; id: string; }; profile: { name: string; age: number; email: string; }; payment_status: 'pending' | 'failed' | 'success' | 'canceled'; } */ export type UserType = ReturnType; ``` ### Using the schema When you create a schema, you will get a nice API to handle multiple use-cases in the client and the server. - `is(data: any): boolean` check if the data is valid (eager evaluation) - `validate(data: any): Errors` errors returned has the same shape as the schema you defined (does not throw) - `produce(data: any): data` throws an error when the data is invalid. otherwise, it returns data - `embed(config?: { optional: boolean })` embeds the schema in other schemas - `source` the schema itself in a parsable format example usage: ```js const Person = createSchema({ name: _.string(), age: _.number(), email: _.string(), }); const john = { name: 'john', age: 42, email: 'john@gmail.com' }; Person.is({}); // false Person.is(john); // true Person.validate({}); // { name: 'invalid-type', age: 'invalid-type', email: 'invalid-type' } Person.validate(john); // null try { Person.produce(undefined); } catch (e) { console.log(e instanceof TypeError); // true console.log(e.message); // "invalid-data" } // embedding the person schema const GroupOfPeople = createSchema({ // ... people: _.listof(Person.embed()), // ... }); ``` ## Validators All validators are required by default. All validators are accessible with the `_` (underscore) namespace; The reason for using `_` instead of a good name like `validators` is developer experience, and you can alias it to whatever you want. ```js import { _ as validators } from 'tiny-schema-validator'; ``` Example of all validators and corresponding Typescript types: ```js import { _ } from 'tiny-schema-validator'; // NOTE: when you call a validator you just create an object // containing { type: '', ...options } // this is just a shorthand for that. // simple validators. _.string(); // string _.number(); // number _.boolean(); // boolean _.constant(42); // 42 // complex validators (types that accepts other types as paramater) _.union( _.record({ id: _.string() }), _.constant(1), _.constant(2), _.constant(3) ); // { id: string; } | 1 | 2 | 3 _.list([ _.number(), _.string(), ]); // [number, number] _.record({ timestamp: _.number(), id: _.string(), }); // { timestamp: number; id: string; } _.listof(_.string()); // string[] _.recordof(_.string()); // Record ``` Check out the full validators API below: | validator | signature | props | | :-------- | ------------------------------- | :------------------------------------------------------------- | | | | | | constant | `constant(value)` | value: `string \| number \| boolean` | | | | | | string | `string(options?)` | options (optional): Object | | | | - `optional : boolean` defaults to false | | | | - `maxLength: [length: number, error: string]` | | | | - `minLength: [length: number, error: string]` | | | | - `pattern : [pattern: RegExp, error: string]` | | | | | | number | `number(options?)` | options(optional): Object | | | | - `optional: boolean` default to false | | | | - `min: [number, error: string]` | | | | - `max: [number, error: string]` | | | | - `is : ['integer' \| 'float', error: string]` default is both | | | | | | boolean | `boolean(options?)` | options(optional): Object | | | | - `optional: boolean` default to false | | | | | | union | `union(...validators)` | validators: Array of validators as paramaters | | | | | | list | `list(validators[], options?)` | validators: Array of validators | | | | options(optional): Object | | | | - `optional: boolean` default to false | | | | | | listof | `listof(validator, options?)` | validator: Validator | | | | options(optional): Object | | | | - `optional: boolean` default to false | | | | | | record | `record(shape, options?)` | shape: `Object { [key: string]: Validator }` | | | | options(optional): Object | | | | - `optional: boolean` default to false | | | | | | recordof | `recordof(validator, options?)` | validator: `Validator` | | | | options(optional): Object | | | | - `optional: boolean` default to false | ### Custom validators To create custom validators that do not break type inference: - use validators from `_` as building blocks for your custom validator. - your custom validator should define `optional` and `required` functions. Example of creating custom validators: ```js const alphaNumeric = (() => { const config = { pattern: [/^[a-zA-Z0-9]*$/, 'only-letters-and-number'], }; return { required: additional => _.string({ ...additional, ...config, optional: false }), // inferred as Required optional: additional => _.string({ ...additional, ...config, optional: true }), // inferred as Optional }; })(); const Person = createSchema({ // ... username: alphaNumeric.required({ maxLength: [20, 'username-too-long'] }), // ... }); ``` ## Built-in Errors ```js // when typeof value does not match the validator infered type const TYPEERR = 'invalid-type'; // when "schema" in createSchema(schema) is not plain object const SCHEMAERR = 'invalid-schema'; // when produce(data) and "data" failed to match the schema // always accompanied be TypeError, so make sure to catch it const DATAERR = 'invalid-data'; // when an unknown key is found in data while using record | list // and any keys that exists on data and not present in the schema const UNKOWN_KEY_ERR = 'unknown-key'; ``` ## Caveats - When using the `recordof | listof | list` validators, the optional property of the validator is ignored, example: ```js _.recordof(_.string({ optional: true /* THIS IS IGNORED */ })); _.list([_.number({ optional: true /* THIS IS IGNORED */ }), _.number()]); ``` - You might expect errors returned from a `list | listof` validators to be an array but it is actually an object, example: ```js const list = createSchema({ list: _.listof(_.string()) }); list.validate({ list: ['string', 42, 'string'] }); // { list: { 1: 'invalid-type' } } ``` ## Recursive types Currently, there's no easy way to create recursive types. if you think you could help, PRs are welcome ## Errors while wrapping schemas if you try to wrap your schema, you will encounter this error (Type instantiation is excessively deep and possibly infinite) to fix it, you should unwrap your schema and re-create it inside your abstraction. let's take the following example: ```ts const User = createSchema({ name: _.string(), age: _.number(), }); // your abstraction function schemaWrapper(schema: T) { //... } const wrappedUser = schemaWrapper(User); // ERROR: Type instantiation is excessively deep and possibly infinite ``` The fix: ```ts import { Schema, R, RecordOptions } from 'tiny-schema-validator'; /* optionally, to infer data from the embedded schema, you do DataFrom import { DataFrom } from 'tiny-schema-validator/dist/type-utils'; */ // extract schema with Schema.embed() function schemaWrapper(schema: R>) { const newSchema = createSchema(schema.shape); // you can add/remove/modify passed schema here // ... } const wrappedUser = schemaWrapper(User.embed()); // all good no errors ``` ================================================ FILE: package.json ================================================ { "version": "5.0.3", "license": "MIT", "main": "dist/index.js", "typings": "dist/index.d.ts", "files": [ "dist", "src" ], "engines": { "node": ">=10" }, "scripts": { "start": "tsdx watch", "build": "tsdx build", "test": "tsdx test", "lint": "tsdx lint", "prepare": "tsdx build", "size": "size-limit", "analyze": "size-limit --why", "release": "standard-version && git push --follow-tags origin master", "release:alpha": "yarn release -- --prerelease alpha" }, "peerDependencies": {}, "husky": { "hooks": { "pre-commit": "tsdx lint" } }, "prettier": { "printWidth": 100, "semi": true, "singleQuote": true, "trailingComma": "es5" }, "name": "tiny-schema-validator", "author": "khaled", "repository": { "url": "https://github.com/5alidz/tiny-schema-validator" }, "module": "dist/tiny-schema-validator.esm.js", "size-limit": [ { "path": "dist/tiny-schema-validator.cjs.production.min.js", "limit": "10 KB" }, { "path": "dist/tiny-schema-validator.esm.js", "limit": "10 KB" } ], "resolutions": { "**/typescript": "^4.0.5", "**/@typescript-eslint/eslint-plugin": "^4.6.1", "**/@typescript-eslint/parser": "^4.6.1" }, "jest": { "coverageReporters": [ "json-summary", "text", "lcov" ] }, "devDependencies": { "@size-limit/preset-small-lib": "^4.9.0", "@typescript-eslint/eslint-plugin": "^4.8.2", "@typescript-eslint/parser": "^4.8.2", "cz-conventional-changelog": "3.3.0", "husky": "^4.3.0", "size-limit": "^4.9.0", "standard-version": "^9.1.1", "tsdx": "^0.14.1", "tslib": "^2.3.1", "typescript": "^4.3.5" }, "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } }, "dependencies": { "tiny-invariant": "^1.1.0" } } ================================================ FILE: src/constants.ts ================================================ export const $string = 'string'; export const $number = 'number'; export const $boolean = 'boolean'; export const $list = 'list'; export const $listof = 'listof'; export const $record = 'record'; export const $recordof = 'recordof'; export const $constant = 'constant'; export const $union = 'union'; export const TYPEERR = 'invalid-type'; export const SCHEMAERR = 'invalid-schema'; export const DATAERR = 'invalid-data'; export const UNKOWN_KEY_ERR = 'unknown-key'; ================================================ FILE: src/createErrors.ts ================================================ import { isPlainObject, isNumber, isString, isBool, ObjectKeys, toObj, isArray } from './utils'; import { BooleanValidator, ConstantValidator, ListofValidator, ListValidator, NumberValidator, RecordofValidator, RecordValidator, Schema, StringValidator, UnionValidator, Validator, } from './validatorTypes'; import { InferResult, InferCallbackResult } from './type-utils'; import { TYPEERR, UNKOWN_KEY_ERR } from './constants'; import invariant from 'tiny-invariant'; function shouldAddToResult(res: unknown) { if ( res == null || (isPlainObject(res) && ObjectKeys(res).length < 1) || (Array.isArray(res) && res.length < 1) ) { return false; } return true; } function shouldSkipValidation(value: unknown, validator: Validator) { return value == null && Boolean(validator.optional); } function normalizeResult>(result: T) { return ObjectKeys(result).length <= 0 ? null : result; } function enterNode(validator: Validator, value: unknown, eager = false) { const fn = validators[validator.type] as any; invariant(typeof fn == 'function', 'invalid-validator-type'); return fn(validator, value, eager); } function parseShapeValidator( validator: RecordValidator | ListValidator, value: unknown, eager = false ) { const shape = toObj(validator).shape; const keys = ObjectKeys(shape); const values = toObj(value); const result: Record = {}; const dataKeys = ObjectKeys(values); for (let i = 0; i < dataKeys.length; i++) { const key = dataKeys[i]; if (!keys.includes(key)) { // we gonna sneak this one in the result without typescript knowning about it. // can be fixed if we require "data" to be also infered // (instead of using any in e.g. schema.validate(data)) result[key] = UNKOWN_KEY_ERR as any; if (eager) return result; } } for (let i = 0; i < keys.length; i++) { const currentResult = enterNode(shape[keys[i]], values[keys[i]]); if (shouldAddToResult(currentResult)) { result[keys[i]] = currentResult; if (eager) return result; } } return normalizeResult(result); } function parseOfValidator( validator: RecordofValidator | ListofValidator, value: unknown, eager = false ) { const values = toObj(value); const keys = ObjectKeys(values); const result: Record = {}; for (let i = 0; i < keys.length; i++) { const currentResult = enterNode(validator.of, values[keys[i]]); if (shouldAddToResult(currentResult)) { result[keys[i]] = currentResult; if (eager) return result; } } return normalizeResult(result); } const validators = { string(validator: StringValidator, value: unknown) { if (shouldSkipValidation(value, validator)) return null; if (!isString(value)) return TYPEERR; const [minLength, minLengthErrMsg] = validator.minLength ? validator.minLength : []; if (minLength && minLengthErrMsg && isNumber(minLength) && value.length < minLength) return minLengthErrMsg; const [maxLength, maxLengthErrMsg] = validator.maxLength ? validator.maxLength : []; if (maxLength && maxLengthErrMsg && isNumber(maxLength) && value.length > maxLength) return maxLengthErrMsg; const [pattern, patterErrMsg] = validator.pattern ? validator.pattern : []; if (pattern && patterErrMsg && pattern.test(value) == false) return patterErrMsg; return null; }, number(validator: NumberValidator, value: unknown) { if (shouldSkipValidation(value, validator)) return null; if (!isNumber(value)) return TYPEERR; const [min, minErrMsg] = validator.min ? validator.min : []; if (isNumber(min) && value < min && minErrMsg) return minErrMsg; const [max, maxErrMsg] = validator.max ? validator.max : []; if (isNumber(max) && value > max && maxErrMsg) return maxErrMsg; const [is, isErrMsg] = validator.is ? validator.is : []; if (isString(is) && isErrMsg) { const isInt = Number.isInteger(value); if ((isInt && is == 'float') || (!isInt && is == 'integer')) return isErrMsg; } return null; }, boolean(validator: BooleanValidator, value: unknown) { if (shouldSkipValidation(value, validator)) return null; if (!isBool(value)) return TYPEERR; return null; }, constant(validator: ConstantValidator, value: unknown) { if (shouldSkipValidation(value, validator)) return null; if (value === validator.value) return null; return TYPEERR; }, union(validator: UnionValidator, value: unknown) { if (shouldSkipValidation(value, validator)) return null; const unionTypes = validator.of; let currentResult = null; for (let i = 0; i < unionTypes.length; i++) { currentResult = enterNode(unionTypes[i], value); if (currentResult == null) return null; } return TYPEERR; }, list(validator: ListValidator, value: unknown, eager = false) { if (shouldSkipValidation(value, validator)) return null; if (!isArray(value)) return TYPEERR; return parseShapeValidator(validator, value, eager); }, listof(validator: ListofValidator, value: unknown, eager = false) { if (shouldSkipValidation(value, validator)) return null; if (!isArray(value)) return TYPEERR; return parseOfValidator(validator, value, eager); }, record(validator: RecordValidator, value: unknown, eager = false) { if (shouldSkipValidation(value, validator)) return null; if (!isPlainObject(value)) return TYPEERR; return parseShapeValidator(validator, value, eager); }, recordof(validator: RecordofValidator, value: unknown, eager = false) { if (shouldSkipValidation(value, validator)) return null; if (!isPlainObject(value)) return TYPEERR; return parseOfValidator(validator, value, eager); }, }; export function createErrors( schema: T, _data: any, eager = false ): null | InferResult { const data = isPlainObject(_data) ? _data : {}; const result: InferResult = {}; const schemaKeys = ObjectKeys(schema) as (keyof T)[]; const dataKeys = ObjectKeys(data); // get unknown keys for (let i = 0; i < dataKeys.length; i++) { const key = dataKeys[i]; if (!schemaKeys.includes(key)) { // we gonna sneak this one in the result without typescript knowning about it. // can be fixed if we require "data" to be also infered // (instead of using any in e.g. schema.validate(data: any)) result[key as keyof T] = UNKOWN_KEY_ERR as any; if (eager) return result; } } for (let i = 0; i < schemaKeys.length; i++) { const schemaKey = schemaKeys[i]; const validator = schema[schemaKey]; const value = data[schemaKey as string]; let _result = enterNode(validator, value, eager); if (shouldAddToResult(_result)) { result[schemaKey] = _result as InferCallbackResult; if (eager) return result; } } return normalizeResult(result); } ================================================ FILE: src/createSchema.ts ================================================ import { isPlainObject } from './utils'; import { createErrors } from './createErrors'; import { DATAERR, $record, SCHEMAERR } from './constants'; import invariant from 'tiny-invariant'; import { RecordValidator, Schema, R, O, RecordOptions } from './validatorTypes'; import { DataFrom } from './type-utils'; export function createSchema(_schema: T) { invariant(isPlainObject(_schema), SCHEMAERR); type Data = DataFrom; const source = Object.freeze({ ..._schema }); function validate(data: any, eager = false) { return createErrors(source, data, eager); } function is(data: any): data is Data { if (!isPlainObject(data)) return false; return validate(data, true) == null; } function embed(): R>; function embed(config: { optional: false }): R>; function embed(config: { optional: true }): O>; function embed(config = { optional: false }): RecordValidator { return { type: $record, shape: source, ...config }; } function produce(data: any): Data { if (!is(data)) throw new TypeError(DATAERR); return data; } return { source, validate, embed, produce, is, }; } ================================================ FILE: src/helpers.ts ================================================ import { O, R, BooleanValidator, ListValidator, ListofValidator, NumberValidator, RecordValidator, RecordofValidator, Schema, StringValidator, Validator, BooleanOptions, ListOptions, ListofOptions, NumberOptions, RecordOptions, RecordofOptions, StringOptions, ConstantOptions, UnionOptions, } from './validatorTypes'; import { $boolean, $constant, $list, $listof, $number, $record, $recordof, $string, $union, } from './constants'; export function string(): R; export function string(config: Omit): R; export function string(config: { optional: false } & Omit): R; export function string(config: { optional: true } & Omit): O; export function string( config?: { optional?: boolean } & Omit ): StringValidator { return { type: $string, optional: !!config?.optional, ...config, }; } export function number(): R; export function number(config: Omit): R; export function number(config: { optional: true } & Omit): O; export function number(config: { optional: false } & Omit): R; export function number( config?: { optional?: boolean } & Omit ): NumberValidator { return { type: $number, optional: !!config?.optional, ...config, }; } export function boolean(): R; export function boolean(config: { optional: true }): O; export function boolean(config: { optional: false }): R; export function boolean(config?: { optional: boolean }): BooleanValidator { return { type: $boolean, optional: !!config?.optional, }; } export function list[]>(list: T): R>; export function list[]>( list: T, config: { optional: false } ): R>; export function list[]>( list: T, config: { optional: true } ): O>; export function list[]>( list: T, config?: { optional: boolean } ): ListValidator { return { type: $list, optional: !!config?.optional, shape: list.map(v => ({ ...v, optional: false })) as T, }; } export function listof>(v: T): R>; export function listof>( v: T, config: { optional: false } ): R>; export function listof>( v: T, config: { optional: true } ): O>; export function listof>( v: T, config?: { optional: boolean } ): ListofValidator { return { type: $listof, optional: !!config?.optional, of: { ...v, optional: false }, }; } export function record(s: T): R>; export function record(s: T, config: { optional: false }): R>; export function record(s: T, config: { optional: true }): O>; export function record(s: T, config?: { optional: boolean }): RecordValidator { return { type: $record, optional: !!config?.optional, shape: s, }; } export function recordof>(v: T): R>; export function recordof>( v: T, config: { optional: false } ): R>; export function recordof>( v: T, config: { optional: true } ): O>; export function recordof>( v: T, config?: { optional: boolean } ): RecordofValidator { return { type: $recordof, of: { ...v, optional: false }, optional: !!config?.optional, }; } export function constant(v: T): R> { return { type: $constant, optional: false, value: v, }; } export function union[]>(...types: T): R> { return { type: $union, optional: false, of: types, }; } ================================================ FILE: src/index.ts ================================================ import * as helpers from './helpers'; export * from './validatorTypes'; export * from './createSchema'; export const _ = helpers; ================================================ FILE: src/type-utils.ts ================================================ import { O, BooleanValidator, ListValidator, ListofValidator, NumberValidator, RecordValidator, RecordofValidator, StringValidator, Schema, ConstantValidator, UnionValidator, Validator, } from './validatorTypes'; type InferTypeWithOptional = T extends O ? U | undefined : U; type ArrayElement = T extends readonly unknown[] ? T extends readonly (infer ElementType)[] ? ElementType : never : never; type InferDataType = T extends UnionValidator ? ArrayElement }>> : T extends ConstantValidator ? U : T extends StringValidator ? InferTypeWithOptional : T extends NumberValidator ? InferTypeWithOptional : T extends BooleanValidator ? InferTypeWithOptional : T extends ListValidator ? InferTypeWithOptional }> : T extends ListofValidator ? InferTypeWithOptional[]> : T extends RecordValidator ? InferTypeWithOptional }> : T extends RecordofValidator ? InferTypeWithOptional }> : never; export type DataFrom = { [K in keyof S]: InferDataType; }; export type InferCallbackResult = V extends | StringValidator | NumberValidator | BooleanValidator | ConstantValidator | UnionValidator ? string : V extends ListValidator ? { [key in number]?: InferCallbackResult } : V extends ListofValidator ? { [key: number]: InferCallbackResult | undefined } : V extends RecordValidator ? { [key in keyof U]?: InferCallbackResult } : V extends RecordofValidator ? { [key: string]: InferCallbackResult | undefined } : never; export type InferResult = { [key in keyof S]?: InferCallbackResult; }; ================================================ FILE: src/utils.ts ================================================ export const ObjectKeys = Object.keys.bind(Object); export const isArray = (value: unknown): value is any[] => Array.isArray(value); export const isBool = (value: unknown): value is boolean => typeof value == 'boolean'; export const isString = (value: unknown): value is string => typeof value == 'string'; export const isNumber = (value: unknown): value is number => typeof value == 'number' && Number.isFinite(value); export function isPlainObject(maybeObject: any): maybeObject is Record { return ( typeof maybeObject == 'object' && maybeObject != null && Object.prototype.toString.call(maybeObject) == '[object Object]' ); } export function toObj(value: any) { return isArray(value) ? { ...value } : isPlainObject(value) ? value : ({} as Record); } ================================================ FILE: src/validatorTypes.ts ================================================ export type O = V & { optional: true }; export type R = V & { optional: false }; export type V = O | R; export interface StringOptions { type: 'string'; maxLength?: [number, string]; minLength?: [number, string]; pattern?: [RegExp, string]; } export interface NumberOptions { type: 'number'; max?: [number, string]; min?: [number, string]; is?: ['integer' | 'float', string]; } export interface BooleanOptions { type: 'boolean'; } export interface ListOptions { type: 'list'; shape: T; } export interface ListofOptions { type: 'listof'; of: T; } export interface RecordOptions { type: 'record'; shape: T; } export interface RecordofOptions { type: 'recordof'; of: T; } export interface ConstantOptions { type: 'constant'; value: T; } export interface UnionOptions { type: 'union'; of: T; } export type BooleanValidator = V; export type StringValidator = V; export type NumberValidator = V; export type ListValidator = V>; export type ListofValidator = V>; export type RecordValidator = V>; export type RecordofValidator = V>; export type ConstantValidator = V>; export type UnionValidator = V>; export type Validator = | UnionValidator | ConstantValidator | StringValidator | NumberValidator | BooleanValidator | ListofValidator | ListValidator | RecordValidator | RecordofValidator; export interface Schema { [key: string]: Validator; } ================================================ FILE: test/index.test.ts ================================================ import { createSchema, _ } from '../src/index'; import { DATAERR, TYPEERR } from '../src/constants'; describe('createSchema throws when', () => { test('passed invalid schema', () => { // @ts-expect-error expect(() => createSchema(null)).toThrow(); // @ts-expect-error expect(() => createSchema(undefined)).toThrow(); // @ts-expect-error expect(() => createSchema([])).toThrow(); }); }); const Person = createSchema({ is_premium: _.boolean({ optional: true }), is_verified: _.boolean(), name: _.string({ maxLength: [24, 'too-long'], minLength: [2, 'too-short'], pattern: [/[a-zA-Z ]/g, 'contains-symbols'], }), age: _.number({ max: [150, 'too-old'], min: [13, 'too-young'], }), email: _.string({ pattern: [/^[^@]+@[^@]+\.[^@]+$/, 'invalid-email'], }), tags: _.listof(_.string(), { optional: true }), friends: _.recordof(_.record({ name: _.string(), id: _.string() }), { optional: true }), nested_list: _.list([_.string(), _.list([_.list([_.number()])])], { optional: true }), four_tags: _.list([_.string(), _.string(), _.string(), _.string(), _.list([_.number()])], { optional: true, }), meta: _.record({ id: _.string({ minLength: [1, 'invalid-id'], maxLength: [1000, 'invalid-id'] }), created: _.number({ is: ['integer', 'timestamp-should-be-intger'] }), updated: _.number({ optional: true }), nested: _.record( { propA: _.number(), propB: _.boolean(), propC: _.string(), }, { optional: true } ), }), payment_status: _.union( _.constant('pending'), _.constant('canceled'), _.constant('processed'), _.constant('failed') ), }); // type IPerson = ReturnType; describe('validate', () => { test('ignores optional properties when not found', () => { const errors = Person.validate({ is_verified: true, name: 'abc', age: 42, email: 'abc@gmail.com', payment_status: 'pending', meta: { id: '123', created: Date.now(), }, }); expect(errors).toBe(null); }); test('validates optional properties when found', () => { const errors = Person.validate({ is_premium: 'hello world', is_verified: true, name: 'abc', age: 42, email: 'abc@gmail.com', tags: {}, meta: { id: '123', created: Date.now(), updated: new Date().toISOString(), }, payment_status: 'pending', }); expect(errors).toStrictEqual({ is_premium: 'invalid-type', tags: 'invalid-type', meta: { updated: 'invalid-type', }, }); }); test('emits correct error messages', () => { const errors = Person.validate( { is_premium: 42, }, true ); expect(errors).toStrictEqual({ is_premium: 'invalid-type' }); }); // test('handles eager validation correctly', () => { // expect(Person.validate({}, true)).toStrictEqual({ name: TYPEERR }); // }); }); describe('produce', () => { const Person = createSchema({ name: _.string(), age: _.number(), email: _.string(), }); test('throws on first error', () => { expect(() => Person.produce(null)).toThrow(new TypeError(DATAERR)); expect(() => Person.produce(undefined)).toThrow(new TypeError(DATAERR)); expect(() => Person.produce(34)).toThrow(new TypeError(DATAERR)); expect(() => Person.produce('hello world')).toThrow(new TypeError(DATAERR)); expect(() => { return Person.produce({ name: 2, age: 42, email: 'email@example.com' }); }).toThrow(new TypeError(DATAERR)); }); test('let data throw if it matches the schema', () => { const p = { name: 'john', age: 42, email: 'john@gmail.com' }; expect(Person.produce(p)).toStrictEqual(p); }); }); describe('is', () => { const s = createSchema({ a: _.record({ b: _.string({ optional: true }), c: _.record({ d: _.number(), e: _.number({ optional: true }) }), }), }); test('returns false when passed incorrect data type', () => { const s = createSchema({}); expect(s.is(undefined)).toBe(false); expect(s.is(null)).toBe(false); expect(s.is([])).toBe(false); expect(s.is({})).toBe(true); }); test('return correct boolean based on data', () => { expect(s.is({ a: { c: { d: 42 } } })).toBe(true); expect(s.is({ a: { b: 'hello', c: { e: 120, d: 42 } } })).toBe(true); expect(s.is({ a: { b: true, c: { e: 120, d: 42 } } })).toBe(false); expect(s.is({ a: { c: { d: 'hello' } } })).toBe(false); }); }); describe('eager validation', () => { const s = createSchema({ a: _.record({ b: _.string({ optional: true }), c: _.record({ d: _.number(), e: _.number({ optional: true }) }), }), }); test('test 1', () => { const errors = s.validate({ a: { b: 42, c: false } }, true); expect(errors).toStrictEqual({ a: { b: TYPEERR } }); }); }); describe('reports unknown keys', () => { describe('is', () => { const schema = createSchema({ hello: _.string(), }); test('exits when it finds an unknown-key', () => { expect(schema.is({ goodbye: 42, hello: 'im valid' })).toBe(false); }); }); describe('validate', () => { const schema = createSchema({ o: _.record({ a: _.string() }), }); test('errors contain unknown-keys error message', () => { expect(schema.validate({ o: { a: '' }, x: 'reported', y: 'reported' })).toStrictEqual({ x: 'unknown-key', y: 'unknown-key', }); const errors = schema.validate({ o: { a: '', b: 42 }, x: 'reported' }); expect(errors).toStrictEqual({ x: 'unknown-key', o: { b: 'unknown-key', }, }); }); test('handles eager validation', () => { expect(schema.validate({ o: { a: '' }, x: 'reported', y: 'reported' }, true)).toStrictEqual({ x: 'unknown-key', }); const errors = schema.validate({ o: { a: '', b: 42 }, x: 'reported' }, true); expect(errors).toStrictEqual({ x: 'unknown-key', }); }); }); test('is', () => { const schema = createSchema({ opt: _.record({}, { optional: true }), list: _.list([_.string(), _.number()], { optional: true }), metadata: _.record({ propA: _.string(), }), }); expect(schema.is({ metadata: { propA: 'hello world' }, unknownKey: 42 })).toBe(false); expect(schema.is({ metadata: { propA: 'hello world', extra: 42 } })).toBe(false); expect(schema.is({ metadata: { propA: 'hello world' }, list: ['hello', 42], opt: {} })).toBe( true ); expect( schema.is({ metadata: { propA: 'hello world' }, list: ['hello', 42], opt: { newKey: 42 } }) ).toBe(false); expect(schema.is({ metadata: { propA: 'hello world' }, list: ['hello', 42, undefined] })).toBe( false ); }); }); ================================================ FILE: test/tsconfig.json ================================================ { "extends": "../tsconfig.json", "include": ["."] } ================================================ FILE: test/validators.test.ts ================================================ import { createSchema, _ } from '../src/index'; import { TYPEERR } from '../src/constants'; const createString = (length: number, char?: string) => { let s = ''; for (let i = 0; i < length; i++) s += char || ' '; return s; }; describe('string validator', () => { const name = (() => { const config = { maxLength: [100, 'too-long'] as [number, string], minLength: [10, 'too-short'] as [number, string], pattern: [/[a-zA-Z]/g, 'invalid-pattern'] as [RegExp, string], }; return { optional: () => _.string({ ...config, optional: true }), required: () => _.string(config), }; })(); const Person1 = createSchema({ name: name.optional(), }); const Person2 = createSchema({ name: name.required(), }); test('tests type', () => { expect(Person1.is({ name: 0 })).toBe(false); expect(Person1.is({ name: 42 })).toBe(false); expect(Person1.is({ name: {} })).toBe(false); expect(Person1.is({ name: [] })).toBe(false); expect(Person1.is({ name: true })).toBe(false); expect(Person2.is({ name: 0 })).toBe(false); expect(Person2.is({ name: 42 })).toBe(false); expect(Person2.is({ name: {} })).toBe(false); expect(Person2.is({ name: [] })).toBe(false); expect(Person2.is({ name: true })).toBe(false); }); test('optional', () => { expect(Person1.is({ name: undefined })).toBe(true); expect(Person1.is({ name: null })).toBe(true); expect(Person1.is({ name: 0 })).toBe(false); expect(Person1.is({ name: '' })).toBe(false); expect(Person2.is({ name: undefined })).toBe(false); expect(Person2.is({ name: null })).toBe(false); expect(Person2.is({ name: 0 })).toBe(false); expect(Person2.is({ name: '' })).toBe(false); }); test('pattern', () => { expect(Person1.is({ name: '0123456789' })).toBe(false); expect(Person1.is({ name: '----------' })).toBe(false); expect(Person1.is({ name: '__________' })).toBe(false); expect(Person1.is({ name: 'abcdefghij' })).toBe(true); expect(Person2.is({ name: '0123456789' })).toBe(false); expect(Person2.is({ name: '----------' })).toBe(false); expect(Person2.is({ name: '__________' })).toBe(false); expect(Person2.is({ name: 'abcdefghij' })).toBe(true); }); test('maxLength', () => { expect(Person1.is({ name: '' })).toBe(false); expect(Person1.is({ name: createString(100, 'a') })).toBe(true); expect(Person1.is({ name: createString(101, 'a') })).toBe(false); }); test('minLength', () => { expect(Person1.is({ name: '' })).toBe(false); expect(Person1.is({ name: createString(9, 'a') })).toBe(false); expect(Person1.is({ name: createString(10, 'a') })).toBe(true); }); test('emits correct error message', () => { expect(Person1.validate({ name: '' })).toStrictEqual({ name: 'too-short' }); expect(Person1.validate({ name: createString(101, 'a') })).toStrictEqual({ name: 'too-long' }); expect(Person1.validate({ name: createString(11, '0') })).toStrictEqual({ name: 'invalid-pattern', }); }); }); describe('number validator', () => { const age = (() => { const config = { max: [10, 'too-large'] as [number, string], min: [1, 'too-small'] as [number, string], is: ['integer', 'wrong-type-of-number'] as ['integer', string], }; return { optional: () => _.number({ ...config, optional: true }), required: () => _.number(config), }; })(); const Person1 = createSchema({ age: age.optional(), n: _.number({ optional: true }), }); const Person2 = createSchema({ age: age.required(), n: _.number({ optional: true }), }); test('tests type', () => { expect(Person1.is({ age: '' })).toBe(false); expect(Person1.is({ age: {} })).toBe(false); expect(Person1.is({ age: [] })).toBe(false); expect(Person1.is({ age: true })).toBe(false); expect(Person1.is({ age: Infinity, n: Infinity })).toBe(false); expect(Person1.is({ age: -Infinity, n: -Infinity })).toBe(false); expect(Person1.is({ age: NaN, n: NaN })).toBe(false); expect(Person2.is({ age: '' })).toBe(false); expect(Person2.is({ age: {} })).toBe(false); expect(Person2.is({ age: [] })).toBe(false); expect(Person2.is({ age: true })).toBe(false); expect(Person1.is({ age: Infinity, n: Infinity })).toBe(false); expect(Person1.is({ age: -Infinity, n: -Infinity })).toBe(false); expect(Person1.is({ age: NaN, n: NaN })).toBe(false); }); test('optional', () => { expect(Person1.is({ age: undefined })).toBe(true); expect(Person1.is({ age: null })).toBe(true); expect(Person1.is({ age: false })).toBe(false); expect(Person2.is({ age: undefined })).toBe(false); expect(Person2.is({ age: null })).toBe(false); expect(Person1.is({ age: false })).toBe(false); }); test('max', () => { expect(Person1.is({ age: 10 })).toBe(true); expect(Person1.is({ age: 11 })).toBe(false); }); test('min', () => { expect(Person1.is({ age: 0 })).toBe(false); expect(Person1.is({ age: 10 })).toBe(true); }); test('is', () => { expect(Person1.is({ age: 9.4 })).toBe(false); expect(Person1.is({ age: 10 })).toBe(true); }); test('emits correct error message', () => { expect(Person1.validate({ age: 11 })).toStrictEqual({ age: 'too-large' }); expect(Person1.validate({ age: -1 })).toStrictEqual({ age: 'too-small' }); expect(Person1.validate({ age: 9.4 })).toStrictEqual({ age: 'wrong-type-of-number' }); }); }); describe('boolean validator', () => { const Person1 = createSchema({ is: _.boolean({ optional: true }), }); const Person2 = createSchema({ is: _.boolean(), }); test('tests type', () => { expect(Person1.is({ is: 0 })).toBe(false); expect(Person1.is({ is: '' })).toBe(false); expect(Person1.is({ is: {} })).toBe(false); expect(Person1.is({ is: [] })).toBe(false); expect(Person2.is({ is: 0 })).toBe(false); expect(Person2.is({ is: '' })).toBe(false); expect(Person2.is({ is: {} })).toBe(false); expect(Person2.is({ is: [] })).toBe(false); expect(Person1.is({ is: false })).toBe(true); expect(Person1.is({ is: true })).toBe(true); expect(Person2.is({ is: false })).toBe(true); expect(Person2.is({ is: true })).toBe(true); }); test('optional', () => { expect(Person1.is({ is: undefined })).toBe(true); expect(Person1.is({ is: null })).toBe(true); expect(Person2.is({ is: undefined })).toBe(false); expect(Person2.is({ is: null })).toBe(false); }); }); describe('listof validator', () => { const Person = createSchema({ friends: _.listof(_.string({ minLength: [2, 'too-short'] })), }); test('emits correct error messages', () => { expect(Person.validate({ friends: [] })).toStrictEqual(null); expect(Person.validate({ friends: {} })).toStrictEqual({ friends: TYPEERR }); expect(Person.validate({ friends: [1, 'john'] })).toStrictEqual({ friends: { 0: TYPEERR } }); }); }); describe('list validator', () => { const Person = createSchema({ friends: _.list([_.string(), _.number()]), otherList: _.list([_.string({ minLength: [4, 'lower-bound'] })], { optional: true }), }); test('handles optional property', () => { expect(Person.validate({ friends: ['John', 42] })).toBe(null); expect(Person.validate({ friends: ['John', 42], otherList: [] })).toStrictEqual({ otherList: { 0: TYPEERR }, }); expect(Person.validate({ friends: ['John', 42], otherList: ['hel'] })).toStrictEqual({ otherList: { 0: 'lower-bound' }, }); expect(Person.validate({ friends: ['John', 42], otherList: ['hell'] })).toStrictEqual(null); }); test('emits correct error messages', () => { expect(Person.validate({ friends: [] })).toStrictEqual({ friends: { 0: TYPEERR, 1: TYPEERR } }); expect(Person.validate({ friends: {} })).toStrictEqual({ friends: TYPEERR }); expect(Person.validate({ friends: [1, 'john'] })).toStrictEqual({ friends: { 0: TYPEERR, 1: TYPEERR }, }); expect(Person.validate({ friends: ['john', 0] })).toStrictEqual(null); }); }); describe('record validator', () => { const Person = createSchema({ meta: _.record({ id: _.string(), date_created: _.number(), is_verified: _.boolean(), }), tags: _.listof(_.string(), { optional: true }), }); test('emits correct error messages', () => { expect(Person.validate({})).toStrictEqual({ meta: TYPEERR }); expect(Person.validate({ meta: {} })).toStrictEqual({ meta: { id: TYPEERR, date_created: TYPEERR, is_verified: TYPEERR, }, }); expect( Person.validate({ meta: { id: 123, date_created: true, is_verified: '' } }) ).toStrictEqual({ meta: { id: TYPEERR, date_created: TYPEERR, is_verified: TYPEERR, }, }); expect( Person.validate({ meta: { id: null, date_created: 123, is_verified: false } }) ).toStrictEqual({ meta: { id: TYPEERR } }); }); test('handle recursive records', () => { const s = createSchema({ o: _.record({ o: _.record({ x: _.record({ y: _.number() }) }) }) }); expect(s.validate({ o: { o: { x: { y: 'hello' } } } })).toStrictEqual({ o: { o: { x: { y: TYPEERR } } }, }); }); }); describe('constant validator', () => { const schema = createSchema({ version: _.constant('v2'), }); test('emits correct error message', () => { expect(schema.validate({ version: 'v2' })).toStrictEqual(null); expect(schema.validate({ version: 'something else' })).toStrictEqual({ version: TYPEERR }); }); }); describe('union validator', () => { test('emits correct error message', () => { const schema = createSchema({ state: _.union(_.constant('on'), _.constant('off'), _.constant('unknown')), }); expect(schema.validate({ state: 'on' })).toStrictEqual(null); expect(schema.validate({ state: 'off' })).toStrictEqual(null); expect(schema.validate({ state: 'unknown' })).toStrictEqual(null); expect(schema.validate({ state: 'should-error' })).toStrictEqual({ state: TYPEERR }); }); test('performs deep checks', () => { const schema = createSchema({ state: _.union( _.record({ prop: _.number() }), _.record({ x: _.record({ nested: _.constant(42) }) }) ), }); expect(schema.validate({ state: { prop: 100 } })).toStrictEqual(null); expect(schema.validate({ state: { x: { nested: 42 } } })).toStrictEqual(null); expect(schema.validate({ state: { x: { nested: 33 } } })).toStrictEqual({ state: TYPEERR }); expect(schema.validate({ state: 'should-error' })).toStrictEqual({ state: TYPEERR }); }); }); describe('recordof validator', () => { const Group = createSchema({ people: _.recordof( _.record({ name: _.string(), age: _.number(), }) ), }); test('emits correct error messages', () => { expect(Group.validate({ people: { john: { name: 'john', age: 42 } } })).toStrictEqual(null); expect( Group.validate({ people: { john: { name: 'john', age: 42 }, sarah: { name: 'sarah', age: true }, }, }) ).toStrictEqual({ people: { sarah: { age: TYPEERR } } }); }); }); ================================================ FILE: tsconfig.json ================================================ { // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs "include": ["src", "types"], "compilerOptions": { "module": "esnext", "lib": ["dom", "esnext"], "importHelpers": true, // output .d.ts declaration files for consumers "declaration": true, // output .js.map sourcemap files for consumers "sourceMap": true, // match output dir to input dir. e.g. dist/index instead of dist/src/index "rootDir": "./src", // stricter type-checking for stronger correctness. Recommended by TS "strict": true, // linter checks for common issues "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative "noUnusedLocals": true, "noUnusedParameters": true, // use Node's module resolution algorithm, instead of the legacy TS one "moduleResolution": "node", // transpile JSX to React.createElement "jsx": "react", // interop between ESM and CJS modules. Recommended by TS "esModuleInterop": true, // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS "skipLibCheck": true, // error out if import and file system have a casing mismatch. Recommended by TS "forceConsistentCasingInFileNames": true, // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` "noEmit": true } }