├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── logo.svg ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.ts ├── src ├── checker.ts ├── index.ts └── types.ts ├── test-d └── index.test-d.ts ├── test └── checker.test.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | build/ 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [main] 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | - name: Setup Node 12 | uses: actions/setup-node@v2 13 | with: 14 | node-version: '14.x' 15 | - name: Install Dependencies 16 | run: npm ci 17 | - name: Lint 18 | run: npm run lint 19 | - name: Test 20 | run: npm run test 21 | - name: Test Definitions 22 | run: npm run test:d 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | build/ 3 | node_modules/ 4 | .idea/ 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('gts/.prettierrc.json') 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.0.1 (2021-06-12) 4 | 5 | - Updated README.md 6 | 7 | ## 1.0.0 (2021-06-01) 8 | 9 | - Released 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 kawmra 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

Typist JSON

6 | 7 |

8 | 9 | minzipped size 10 | 11 | types 12 | 13 | license 14 | 15 | 16 | ci 17 | 18 |

19 | 20 |

21 | A simple runtime JSON type checker. 22 |

23 | 24 | # Features 25 | 26 | - **Simple**. No JSON Schema, No validation rules 27 | - **Type-safe**. Written in TypeScript 28 | - **Intuitive**. Familiar syntax like TypeScript interface 29 | 30 | Typist JSON is focused on type checking, so there is no validation rules like range of numbers or length of strings. 31 | 32 | # Install 33 | 34 | ```shell 35 | npm install typist-json 36 | ``` 37 | 38 | **NOTE:** Require TypeScript 4.1 or higher because Typist JSON uses [`Key Remapping`](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#key-remapping-via-as) and [`Template Literal Types`](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html). 39 | 40 | # Example 41 | 42 | ```typescript 43 | import { j } from "typist-json"; 44 | 45 | const NameJson = j.object({ 46 | firstname: j.string, 47 | lastname: j.string, 48 | }); 49 | 50 | const UserJson = j.object({ 51 | name: NameJson, 52 | age: j.number, 53 | "nickname?": j.string, // optional property 54 | }); 55 | 56 | const userJson = await fetch("/api/user") 57 | .then(res => res.json()); 58 | 59 | if (UserJson.check(userJson)) { 60 | // now, the userJson is narrowed to: 61 | // { 62 | // name: { 63 | // firstname: string 64 | // lastname: string 65 | // } 66 | // age: number 67 | // nickname?: string | undefined 68 | // } 69 | } 70 | ``` 71 | 72 | ## Circular References 73 | 74 | Sometimes JSON structures can form circular references. 75 | 76 | Typist JSON can represent circular references by wrapping checkers in the arrow function. 77 | 78 | ```ts 79 | const FileJson = j.object({ 80 | filename: j.string, 81 | }); 82 | 83 | const DirJson = j.object({ 84 | dirname: j.string, 85 | entries: () => j.array(j.any([FileJson, DirJson])), // references itself 86 | }); 87 | 88 | DirJson.check({ 89 | dirname: "animals", 90 | entries: [ 91 | { 92 | dirname: "cat", 93 | entries: [ 94 | { filename: "american-shorthair.jpg" }, 95 | { filename: "munchkin.jpg" }, 96 | { filename: "persian.jpg" }, 97 | ], 98 | }, 99 | { 100 | dirname: "dog", 101 | entries: [ 102 | { filename: "chihuahua.jpg" }, 103 | { filename: "pug.jpg" }, 104 | { filename: "shepherd.jpg" }, 105 | ], 106 | }, 107 | { filename: "turtle.jpg" }, 108 | { filename: "shark.jpg" }, 109 | ], 110 | }); // true 111 | ``` 112 | 113 | # Type checkers 114 | 115 | ## Strings 116 | 117 | ```ts 118 | j.string.check("foo"); // true 119 | j.string.check("bar"); // true 120 | ``` 121 | 122 | ## Numbers 123 | 124 | ```ts 125 | j.number.check(42); // true 126 | j.number.check(12.3); // true 127 | j.number.check("100"); // false 128 | ``` 129 | 130 | ## Booleans 131 | 132 | ```ts 133 | j.boolean.check(true); // true 134 | j.boolean.check(false); // true 135 | ``` 136 | 137 | ## Literals 138 | 139 | ```ts 140 | j.literal("foo").check("foo"); // true 141 | j.literal("foo").check("fooooo"); // false 142 | ``` 143 | 144 | ## Arrays 145 | 146 | ```ts 147 | j.array(j.string).check(["foo", "bar"]); // true 148 | j.array(j.string).check(["foo", 42]); // false 149 | j.array(j.string).check([]); // true 150 | j.array(j.number).check([]); // true 151 | ``` 152 | 153 | ## Objects 154 | 155 | ```ts 156 | j.object({ 157 | name: j.string, 158 | age: j.number, 159 | "nickname?": j.string, 160 | }).check({ 161 | name: "John", 162 | age: 20, 163 | nickname: "Johnny", 164 | }); // true 165 | 166 | j.object({ 167 | name: j.string, 168 | age: j.number, 169 | "nickname?": j.string, 170 | }).check({ 171 | name: "Emma", 172 | age: 20, 173 | }); // true, since "nickname" is optional 174 | 175 | j.object({ 176 | name: j.string, 177 | age: j.number, 178 | "nickname?": j.string, 179 | }).check({ 180 | id: "xxxx", 181 | type: "android", 182 | }); // false, since "name" and "age" is required 183 | ``` 184 | 185 | If a property that ends with `?` is not optional, you should replace all trailing `?` by `??`. 186 | 187 |
188 | More details about escaping 189 | 190 | As mentioned above, you need to escape all trailing `?` as `??`. 191 | 192 | ```ts 193 | j.object({ 194 | "foo??": j.boolean, 195 | }).check({ 196 | "foo?": true, 197 | }); // true 198 | ``` 199 | 200 | So if you want optional property with a name `"foo???"`, 201 | you should use `"foo???????"` as key. 202 | 203 | ```ts 204 | j.object({ 205 | "foo???????": j.boolean, 206 | }).check({}); // true, since "foo???" is optional 207 | ``` 208 |
209 | 210 | ## Nulls 211 | 212 | ```ts 213 | j.nil.check(null); // true 214 | j.nil.check(undefined); // false 215 | ``` 216 | 217 | ## Nullables 218 | 219 | ```ts 220 | j.nullable(j.string).check("foo"); // true 221 | j.nullable(j.string).check(null); // true 222 | j.nullable(j.string).check(undefined); // false 223 | ``` 224 | 225 | ## Unknowns 226 | 227 | ```ts 228 | j.unknown.check("foo"); // true 229 | j.unknown.check(123); // true 230 | j.unknown.check(null); // true 231 | j.unknown.check(undefined); // true 232 | j.unknown.check([{}, 123, false, "foo"]); // true 233 | ``` 234 | 235 | ## Unions 236 | 237 | ```ts 238 | j.any([j.string, j.boolean]).check(false); // true 239 | 240 | j.any([ 241 | j.literal("foo"), 242 | j.literal("bar"), 243 | ]).check("foo"); // true 244 | ``` 245 | 246 | # Get JSON type of checkers 247 | 248 | ```ts 249 | import { j, JsonTypeOf } from "typist-json"; 250 | 251 | const UserJson = j.object({ 252 | name: j.string, 253 | age: j.number, 254 | "nickname?": j.string, 255 | }); 256 | 257 | type UserJsonType = JsonTypeOf; 258 | // 259 | // ^ This is same as: 260 | // 261 | // type UserJsonType = { 262 | // name: string; 263 | // age: number; 264 | // nickname?: string; 265 | // } 266 | ``` 267 | -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | transform: { 4 | '^.+\\.(ts|tsx)$': 'ts-jest', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typist-json", 3 | "version": "1.0.1", 4 | "description": "A simple runtime JSON type checker", 5 | "main": "./lib/index.js", 6 | "module": "./lib/index.esm.js", 7 | "types": "./lib/index.d.ts", 8 | "files": [ 9 | "lib" 10 | ], 11 | "scripts": { 12 | "test": "jest", 13 | "test:d": "npm run build && tsd", 14 | "lint": "eslint", 15 | "clean": "rimraf lib", 16 | "fix": "eslint --fix", 17 | "build": "rollup --config rollup.config.ts" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/kawmra/typist-json.git" 22 | }, 23 | "keywords": [ 24 | "json", 25 | "type-checker", 26 | "typescript" 27 | ], 28 | "author": "kawmra", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/kawmra/typist-json/issues" 32 | }, 33 | "homepage": "https://github.com/kawmra/typist-json#readme", 34 | "devDependencies": { 35 | "@rollup/plugin-typescript": "^8.2.1", 36 | "@types/jest": "^26.0.20", 37 | "@types/node": "^14.11.2", 38 | "gts": "^3.1.0", 39 | "jest": "^26.6.3", 40 | "rimraf": "^3.0.2", 41 | "rollup": "^2.40.0", 42 | "ts-jest": "^26.5.3", 43 | "tsd": "^0.14.0", 44 | "tslib": "^2.1.0", 45 | "typescript": "^4.1.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | 3 | export default { 4 | input: 'src/index.ts', 5 | output: [ 6 | { 7 | dir: 'lib', 8 | entryFileNames: 'index.js', 9 | format: 'cjs', 10 | }, 11 | { 12 | dir: 'lib', 13 | entryFileNames: 'index.esm.js', 14 | format: 'esm', 15 | }, 16 | ], 17 | plugins: [typescript()], 18 | }; 19 | -------------------------------------------------------------------------------- /src/checker.ts: -------------------------------------------------------------------------------- 1 | import {Checker, JsonTypeOf} from './types'; 2 | 3 | export const string: Checker = { 4 | check: (value: unknown): value is string => { 5 | return typeof value === 'string'; 6 | }, 7 | }; 8 | 9 | export const number: Checker = { 10 | check: (value: unknown): value is number => { 11 | return typeof value === 'number'; 12 | }, 13 | }; 14 | 15 | export const boolean: Checker = { 16 | check: (value: unknown): value is boolean => { 17 | return typeof value === 'boolean'; 18 | }, 19 | }; 20 | 21 | export const nil: Checker = { 22 | check: (value: unknown): value is null => { 23 | return value === null; 24 | }, 25 | }; 26 | 27 | export const unknown: Checker = { 28 | check: (value: unknown): value is unknown => { 29 | return true; 30 | }, 31 | }; 32 | 33 | export function literal(str: T): Checker { 34 | return { 35 | check(value: unknown): value is T { 36 | return value === str; 37 | }, 38 | }; 39 | } 40 | 41 | export function any>( 42 | checkers: T[] 43 | ): Checker> { 44 | return { 45 | check(value: unknown): value is JsonTypeOf { 46 | return checkers.some(checker => checker.check(value)); 47 | }, 48 | }; 49 | } 50 | 51 | export function nullable(checker: Checker): Checker { 52 | return { 53 | check(value: unknown): value is T | null { 54 | return value === null || checker.check(value); 55 | }, 56 | }; 57 | } 58 | 59 | export function array>( 60 | checker: T 61 | ): Checker[]> { 62 | return { 63 | check(value: unknown): value is JsonTypeOf[] { 64 | return Array.isArray(value) && value.every(it => checker.check(it)); 65 | }, 66 | }; 67 | } 68 | 69 | export function object | Function}>( 70 | properties: T 71 | ): Checker>> { 72 | return { 73 | check(value: unknown): value is ExpandRecursively> { 74 | if (typeof value !== 'object' || value === null) return false; 75 | return Object.keys(properties).every(key => { 76 | const unescapedKey = unescapePropertyName(key); 77 | if (has(value, unescapedKey)) { 78 | const checkerOrFunction = properties[key]; 79 | if (isChecker(checkerOrFunction)) { 80 | return checkerOrFunction.check(value[unescapedKey]); 81 | } 82 | const lazyChecker: unknown = checkerOrFunction(); 83 | if (isChecker(lazyChecker)) { 84 | return lazyChecker.check(value[unescapedKey]); 85 | } else { 86 | return false; 87 | } 88 | } else { 89 | return isOptionalProperty(key); 90 | } 91 | }); 92 | }, 93 | }; 94 | } 95 | 96 | function isChecker(target: unknown): target is Checker { 97 | return ( 98 | typeof target === 'object' && 99 | target !== null && 100 | has(target, 'check') && 101 | typeof target.check === 'function' 102 | ); 103 | } 104 | 105 | type FilterOptionalPropertyName = T extends `${infer U}??` 106 | ? `${FilterOptionalPropertyName}?` 107 | : T extends `${infer U}?` 108 | ? U 109 | : never; 110 | 111 | type FilterRequiredPropertyName = T extends `${infer U}??` 112 | ? `${FilterRequiredPropertyName}?` 113 | : T extends `${infer U}?` 114 | ? never 115 | : T; 116 | 117 | type ObjectJsonTypeOf | Function}> = { 118 | [K in keyof T as FilterRequiredPropertyName]-?: T[K] extends Checker 119 | ? JsonTypeOf 120 | : T[K] extends Function 121 | ? LazyChecker 122 | : never; 123 | } & 124 | { 125 | [K in keyof T as FilterOptionalPropertyName]+?: T[K] extends Checker 126 | ? JsonTypeOf 127 | : T[K] extends Function 128 | ? LazyChecker 129 | : never; 130 | }; 131 | 132 | type LazyChecker = T extends () => Checker 133 | ? U 134 | : never; 135 | 136 | type ExpandRecursively = T extends object 137 | ? T extends infer O 138 | ? {[K in keyof O]: ExpandRecursively} 139 | : never 140 | : T; 141 | 142 | export function isOptionalProperty(propertyName: string): boolean { 143 | return propertyName.match(/[^?]?(?:\?\?)*(\?)?$/)?.[1] === '?'; 144 | } 145 | 146 | export function unescapePropertyName(propertyName: string): string { 147 | const match = propertyName.match(/^(.*?[^?]?)((?:\?\?)*)\??$/); 148 | if (match === null) { 149 | return propertyName; 150 | } 151 | const head = match[1] ?? ''; 152 | const questionMarks = match[2].replace(/\?\?/g, '?') ?? ''; 153 | return `${head}${questionMarks}`; 154 | } 155 | 156 | // https://github.com/microsoft/TypeScript/issues/21732#issuecomment-663994772 157 | function has

( 158 | target: object, 159 | property: P 160 | ): target is {[K in P]: unknown} { 161 | return property in target; 162 | } 163 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {Checker, JsonTypeOf} from './types'; 2 | export * as j from './checker'; 3 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Checker { 2 | check(value: unknown): value is T; 3 | } 4 | 5 | export type JsonTypeOf> = T extends Checker 6 | ? U 7 | : never; 8 | -------------------------------------------------------------------------------- /test-d/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectNotType, expectType} from 'tsd'; 2 | import {Checker, j} from '../lib'; 3 | 4 | // string 5 | expectType>(j.string); 6 | 7 | // number 8 | expectType>(j.number); 9 | 10 | // boolean 11 | expectType>(j.boolean); 12 | 13 | // null 14 | expectType>(j.nil); 15 | 16 | // unknown 17 | expectType>(j.unknown); 18 | 19 | // literal 20 | expectType>(j.literal('literal')); 21 | 22 | // any 23 | expectType< 24 | Checker< 25 | | string 26 | | number 27 | | boolean 28 | | null 29 | | 'literal' 30 | | (number | boolean) 31 | | string[] 32 | | {foo: string} 33 | > 34 | >( 35 | j.any([ 36 | j.string, 37 | j.number, 38 | j.boolean, 39 | j.nil, 40 | j.literal('literal'), 41 | j.any([j.number, j.boolean]), 42 | j.array(j.string), 43 | j.object({foo: j.string}), 44 | ]) 45 | ); 46 | expectType>(j.any([j.string, j.unknown, j.number])); 47 | 48 | // nullable 49 | expectType>(j.nullable(j.string)); 50 | expectType>(j.nullable(j.array(j.string))); 51 | expectType>( 52 | j.nullable(j.object({foo: j.string})) 53 | ); 54 | expectType>(j.nullable(j.unknown)); 55 | 56 | // array 57 | expectType>(j.array(j.string)); 58 | expectType>(j.array(j.number)); 59 | expectType>(j.array(j.boolean)); 60 | expectType>(j.array(j.nil)); 61 | expectType>(j.array(j.unknown)); 62 | expectType>(j.array(j.literal('literal'))); 63 | expectType>(j.array(j.any([j.string, j.number]))); 64 | expectType>(j.array(j.array(j.string))); 65 | expectType>(j.array(j.object({foo: j.string}))); 66 | 67 | // object 68 | expectType>(j.object({})); 69 | expectType>(j.object({foo: () => j.string})); 70 | expectType>(j.object({foo: () => 'foo'})); 71 | expectType>(j.object({'optional?': j.string})); 72 | expectNotType>( 73 | j.object({'optional?': j.string}) 74 | ); 75 | expectType>(j.object({'escaped??': j.string})); 76 | expectType>( 77 | j.object({'escaped_optional???': j.string}) 78 | ); 79 | expectNotType>( 80 | j.object({'escaped_optional???': j.string}) 81 | ); 82 | expectType< 83 | Checker<{ 84 | string: string; 85 | number: number; 86 | boolean: boolean; 87 | nil: null; 88 | unknown: unknown; 89 | literal: 'literal'; 90 | union: string | number; 91 | nullable: string | null; 92 | array: string[]; 93 | nested: { 94 | string: string; 95 | }; 96 | optional?: string; 97 | 'escaped?': string; 98 | 'escaped_optional?'?: string; 99 | lazy: number | string; 100 | }> 101 | >( 102 | j.object({ 103 | string: j.string, 104 | number: j.number, 105 | boolean: j.boolean, 106 | nil: j.nil, 107 | unknown: j.unknown, 108 | literal: j.literal('literal'), 109 | union: j.any([j.string, j.number]), 110 | nullable: j.nullable(j.string), 111 | array: j.array(j.string), 112 | nested: j.object({ 113 | string: j.string, 114 | }), 115 | 'optional?': j.string, 116 | 'escaped??': j.string, 117 | 'escaped_optional???': j.string, 118 | lazy: () => j.any([j.number, j.string]), 119 | }) 120 | ); 121 | -------------------------------------------------------------------------------- /test/checker.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | any, 3 | array, 4 | boolean, 5 | isOptionalProperty, 6 | literal, 7 | nil, 8 | nullable, 9 | number, 10 | object, 11 | string, 12 | unescapePropertyName, 13 | unknown, 14 | } from '../src/checker'; 15 | import {Checker} from '../src'; 16 | 17 | export class TestChecker implements Checker { 18 | isCalled = false; 19 | private readonly result: boolean; 20 | 21 | constructor(result: boolean) { 22 | this.result = result; 23 | } 24 | 25 | check(value: unknown): value is unknown { 26 | this.isCalled = true; 27 | return this.result; 28 | } 29 | } 30 | 31 | export class TestSequenceChecker implements Checker { 32 | private callCount = 0; 33 | private readonly results: boolean[]; 34 | 35 | constructor(results: boolean[]) { 36 | this.results = results; 37 | } 38 | 39 | check(value: unknown): value is unknown { 40 | return this.results[this.callCount++]; 41 | } 42 | } 43 | 44 | describe('checker', () => { 45 | test('string', () => { 46 | expect(string.check('foo')).toBe(true); 47 | expect(string.check(123)).toBe(false); 48 | expect(string.check({})).toBe(false); 49 | expect(string.check(null)).toBe(false); 50 | }); 51 | 52 | test('number', () => { 53 | expect(number.check(123)).toBe(true); 54 | expect(number.check(Number.POSITIVE_INFINITY)).toBe(true); 55 | expect(number.check(Number.NEGATIVE_INFINITY)).toBe(true); 56 | expect(number.check(Number.NaN)).toBe(true); 57 | expect(number.check('123')).toBe(false); 58 | expect(number.check({})).toBe(false); 59 | expect(number.check(null)).toBe(false); 60 | }); 61 | 62 | test('boolean', () => { 63 | expect(boolean.check(true)).toBe(true); 64 | expect(boolean.check(false)).toBe(true); 65 | expect(boolean.check('true')).toBe(false); 66 | expect(boolean.check(1)).toBe(false); 67 | expect(boolean.check({})).toBe(false); 68 | expect(boolean.check([])).toBe(false); 69 | expect(boolean.check(null)).toBe(false); 70 | }); 71 | 72 | test('nil', () => { 73 | expect(nil.check(null)).toBe(true); 74 | expect(nil.check('null')).toBe(false); 75 | expect(nil.check(Number.NaN)).toBe(false); 76 | expect(nil.check({})).toBe(false); 77 | expect(nil.check([])).toBe(false); 78 | expect(nil.check(undefined)).toBe(false); 79 | }); 80 | 81 | test('unknown', () => { 82 | expect(unknown.check('foo')).toBe(true); 83 | expect(unknown.check(123)).toBe(true); 84 | expect(unknown.check({})).toBe(true); 85 | expect(unknown.check([])).toBe(true); 86 | expect(unknown.check(true)).toBe(true); 87 | expect(unknown.check(false)).toBe(true); 88 | expect(unknown.check(null)).toBe(true); 89 | }); 90 | 91 | test('literal', () => { 92 | expect(literal('foo').check('foo')).toBe(true); 93 | expect(literal('foo').check('fooo')).toBe(false); 94 | }); 95 | 96 | describe('any', () => { 97 | it('should return true if any of given checkers return true', () => { 98 | const checker1 = new TestChecker(true); 99 | const checker2 = new TestChecker(false); 100 | const checker3 = new TestChecker(false); 101 | expect(any([checker1, checker2]).check(null)).toBe(true); 102 | expect(any([checker1, checker3]).check(null)).toBe(true); 103 | expect(any([checker2, checker3]).check(null)).toBe(false); 104 | }); 105 | }); 106 | 107 | describe('nullable', () => { 108 | it('should return true if the value to be checked is null', () => { 109 | const checker = new TestChecker(false); 110 | expect(nullable(checker).check(null)).toBe(true); 111 | }); 112 | 113 | it('should return true if the given checker returns true', () => { 114 | const checker = new TestChecker(true); 115 | expect(nullable(checker).check('any values')).toBe(true); 116 | expect(checker.isCalled).toBe(true); 117 | }); 118 | }); 119 | 120 | describe('array', () => { 121 | it('should return true if all elements are valid type', () => { 122 | const checker = new TestSequenceChecker([true, true, true, true, true]); 123 | expect(array(checker).check(Array(5).fill(null))).toBe(true); 124 | }); 125 | 126 | it('should return false if even one element is wrong type', () => { 127 | const checker = new TestSequenceChecker([true, false, true]); 128 | expect(array(checker).check(Array(3).fill(null))).toBe(false); 129 | }); 130 | }); 131 | 132 | describe('object', () => { 133 | it('should return true if all properties are valid type', () => { 134 | expect( 135 | object({ 136 | foo: new TestChecker(true), 137 | bar: new TestChecker(true), 138 | }).check({foo: null, bar: null}) 139 | ).toBe(true); 140 | }); 141 | 142 | it('should return false if even one property is wrong type', () => { 143 | expect( 144 | object({ 145 | foo: new TestChecker(true), 146 | bar: new TestChecker(false), 147 | }).check({ 148 | foo: null, 149 | bar: null, 150 | }) 151 | ).toBe(false); 152 | }); 153 | 154 | it('should return true if lazy checker returns true', () => { 155 | expect( 156 | object({ 157 | foo: () => new TestChecker(true), 158 | }).check({foo: null}) 159 | ).toBe(true); 160 | }); 161 | 162 | it('should return false if lazy checker returns false', () => { 163 | expect( 164 | object({ 165 | foo: () => new TestChecker(false), 166 | }).check({foo: null}) 167 | ).toBe(false); 168 | }); 169 | 170 | it('should return false if function does not return checker', () => { 171 | expect( 172 | object({ 173 | foo: () => 'foo', 174 | }).check({foo: 'foo'}) 175 | ).toBe(false); 176 | expect( 177 | object({ 178 | foo: () => null, 179 | }).check({foo: null}) 180 | ).toBe(false); 181 | }); 182 | 183 | it('should return false if required property is missing', () => { 184 | expect(object({foo: new TestChecker(true)}).check({})).toBe(false); 185 | expect(object({'bar??': new TestChecker(true)}).check({})).toBe(false); 186 | expect(object({'foobar????????': new TestChecker(true)}).check({})).toBe( 187 | false 188 | ); 189 | }); 190 | 191 | it('should return true if optional property is missing', () => { 192 | expect( 193 | object({ 194 | 'foo?': new TestChecker(false), 195 | 'bar???': new TestChecker(false), 196 | 'foobar???????': new TestChecker(false), 197 | }).check({}) 198 | ).toBe(true); 199 | }); 200 | }); 201 | }); 202 | 203 | describe('isOptionalProperty', () => { 204 | test('empty', () => { 205 | expect(isOptionalProperty('')).toBe(false); 206 | expect(isOptionalProperty('?')).toBe(true); 207 | expect(isOptionalProperty('??')).toBe(false); 208 | expect(isOptionalProperty('???')).toBe(true); 209 | expect(isOptionalProperty('????')).toBe(false); 210 | expect(isOptionalProperty('?????')).toBe(true); 211 | }); 212 | 213 | test('name', () => { 214 | expect(isOptionalProperty('foo')).toBe(false); 215 | expect(isOptionalProperty('foo?')).toBe(true); 216 | expect(isOptionalProperty('foo??')).toBe(false); 217 | expect(isOptionalProperty('foo???')).toBe(true); 218 | expect(isOptionalProperty('foo????')).toBe(false); 219 | expect(isOptionalProperty('foo?????')).toBe(true); 220 | }); 221 | 222 | test('name with question mark', () => { 223 | expect(isOptionalProperty('foo?bar')).toBe(false); 224 | expect(isOptionalProperty('foo?bar?')).toBe(true); 225 | expect(isOptionalProperty('foo?bar??')).toBe(false); 226 | expect(isOptionalProperty('foo?bar???')).toBe(true); 227 | expect(isOptionalProperty('foo?bar????')).toBe(false); 228 | expect(isOptionalProperty('foo?bar?????')).toBe(true); 229 | }); 230 | }); 231 | 232 | describe('unescapePropertyName', () => { 233 | test('empty', () => { 234 | expect(unescapePropertyName('')).toBe(''); 235 | expect(unescapePropertyName('?')).toBe(''); 236 | expect(unescapePropertyName('??')).toBe('?'); 237 | expect(unescapePropertyName('???')).toBe('?'); 238 | expect(unescapePropertyName('????')).toBe('??'); 239 | expect(unescapePropertyName('?????')).toBe('??'); 240 | }); 241 | 242 | test('name', () => { 243 | expect(unescapePropertyName('foo')).toBe('foo'); 244 | expect(unescapePropertyName('foo?')).toBe('foo'); 245 | expect(unescapePropertyName('foo??')).toBe('foo?'); 246 | expect(unescapePropertyName('foo???')).toBe('foo?'); 247 | expect(unescapePropertyName('foo????')).toBe('foo??'); 248 | expect(unescapePropertyName('foo?????')).toBe('foo??'); 249 | }); 250 | 251 | test('name with question mark', () => { 252 | expect(unescapePropertyName('foo?bar')).toBe('foo?bar'); 253 | expect(unescapePropertyName('foo?bar?')).toBe('foo?bar'); 254 | expect(unescapePropertyName('foo?bar??')).toBe('foo?bar?'); 255 | expect(unescapePropertyName('foo?bar???')).toBe('foo?bar?'); 256 | expect(unescapePropertyName('foo?bar????')).toBe('foo?bar??'); 257 | expect(unescapePropertyName('foo?bar?????')).toBe('foo?bar??'); 258 | }); 259 | }); 260 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "strict": true, 5 | "target": "ES5", 6 | "lib": ["es2015"], 7 | "module": "esnext", 8 | "moduleResolution": "Node", 9 | "declaration": true, 10 | "declarationDir": "lib", 11 | }, 12 | "include": [ 13 | "./src/**/*" 14 | ] 15 | } 16 | --------------------------------------------------------------------------------