├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── CHANGELOG.md ├── lib │ ├── index.ts │ ├── match-dsl.ts │ └── matchers.ts └── test │ └── index.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | dist/ 4 | node_modules/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .gitignore 4 | .npmignore 5 | node_modules/ 6 | src/ 7 | tsconfig.json 8 | dist/test/ 9 | .travis.yml 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: false 7 | node_js: 8 | - '8.11' 9 | before_script: 10 | - npm install 11 | script: 12 | - npm run test && npm run build && npm run build 13 | deploy: 14 | provider: npm 15 | on: 16 | tags: true 17 | skip_cleanup: true 18 | email: lostintime.dev@gmail.com 19 | api_key: 20 | secure: AxnvL95NHqPRehJSPmBsxu8DUyeNea9SlHtrsy3U7RXwSCDAgAZ+SbaYbKuZw9IP5rASfDz0YrHGWlUpr1372WGP5dKF1IimCFL2vyj5TUchG0sHCQKGQub0qgPz0gEaarhpQMhK57Wo4/4t3x/FK5nROwdBzdNgo5x5XUMLQxxYIOs/FvZhsOYW6xRgwV+/Rk1/37lAzO2nucSfnes28TTpKfOCFCnIhSs0rn1gvJ+6yLtwSSq1w+1F5Ve+gVm+oswBPzLcnUUbkE/1ldZOxBcI6aYlZdolGrnaM7jInc9V7hJZAomJBH1NzMOzjGbAxeBizLi4aSnH2Ci7+R8ZEFiv5UaCY3onF312Et8YOZ3k0/iI31CCGKtkL23XNwhW8nWJ8R0LlHQfvVgVxIenOKHdv0XhJh2NQyroxSr6Brnct8mjfc4TjdSIrRgV0L004c8ril3E7rDS1WzWtHHiwU/KhDqcbMrDbh4qoppqsB+weyepnzqLwRKhJOTl2/Cvu4DLcgUV/FyOapDqK2zM/ZVTCH3UAsg3aIsZ3Bxnwo/l3wPQ3jiXMS/jo6BTL4nQX06xllYF2R9lpfw3lOZ0eABPzZ8WPLGYyr46XxnrXkJunhCuTpW/RqNGqta5Vfgo7KY8KF/i1gnuUGp1oqPmqjAZ96XtN9xi/JCBnqkqFu0= 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 by TypeMatcher developers 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TypeMatcher 2 | =========== 3 | 4 | Type matching library for [TypeScript](http://www.typescriptlang.org/). 5 | 6 | [TypeScript](http://www.typescriptlang.org/) is doing great job by bringing type safety 7 | to JavaScript land, It will save you from writing lots of checks which can be defined 8 | as types and _executed_ at compilation time. But all of this is only true while your code users 9 | are also in safe environment, from Input to Output, so you functions relying on 10 | _type safe_ inputs will not crack. 11 | 12 | This library provides constructions to _cover your input_ with type checks and pattern-match on them. 13 | 14 | ## Installation 15 | 16 | ``` 17 | npm install --save typematcher 18 | ``` 19 | 20 | ## Usage examples 21 | 22 | TypeMatcher library contains 2 main components: 23 | 24 | * type matchers - functions to check value matches a type 25 | * matching dsl - constructs to map a type A to B using type matchers to refine 26 | 27 | `TypeMatcher` is a type alias for a function returning `true` if its argument type matches: 28 | 29 | ```typescript 30 | type TypeMatcher = (val: any) => val is T 31 | ``` 32 | 33 | Some type matchers: `isString`, `isNumber`, `isArrayOf`, `isTuple1`. 34 | 35 | Matching DSL consists of few functions: `match`, `caseWhen` and `caseDefault`: 36 | 37 | Exhaustive match: 38 | 39 | ```typescript 40 | import { match, caseWhen, isString } from "typematcher" 41 | 42 | const x: number = match("1" as string | number, 43 | caseWhen(isNumber, _ => _). 44 | caseWhen(isString, _ => parseInt(_, 10)) 45 | ) 46 | ``` 47 | 48 | Default case handler: 49 | 50 | ```typescript 51 | import { match, caseWhen, isBoolean } from "typematcher" 52 | 53 | const x: 1 | 0 = match("2" as string | number, 54 | caseWhen(isBoolean, _ => _ ? 1 : 0). 55 | caseWhen(isNumber, _ => _ > 0 ? 1 : 0). 56 | // string type not covered, default case required 57 | caseDefault(() => 0) 58 | ) 59 | ``` 60 | 61 | Composing type matchers: 62 | 63 | ```typescript 64 | import { 65 | match, caseWhen, caseDefault, isValue, hasFields, isString, isOptional, isNumber, 66 | isEither, isNull 67 | } from 'typematcher' 68 | 69 | enum UserRole { 70 | Member = 0, 71 | Moderator = 1, 72 | Admin = 2 73 | } 74 | 75 | type User = { 76 | name: string, 77 | role: UserRole, 78 | age?: number 79 | } 80 | 81 | /** 82 | * UserRole type matcher 83 | */ 84 | function isUserRole(val: any): val is UserRole { 85 | switch (val) { 86 | case UserRole.Member: 87 | case UserRole.Moderator: 88 | case UserRole.Admin: 89 | return true 90 | default: 91 | return false 92 | } 93 | } 94 | 95 | const isUser: TypeMatcher = hasFields({ 96 | name: isString, 97 | role: isUserRole, 98 | age: isOptional(isNumber) 99 | }) 100 | 101 | const user: any = { name: "John", role: 20 } 102 | 103 | const u: User | null = match(user, 104 | caseWhen(isEither(isUser, isNull), _ => _). 105 | caseDefault(() => null) 106 | ) 107 | ``` 108 | Sometimes is simpler to use `switch/case` but unfortunately not as an expression. 109 | 110 | For more examples - check links in documentation section. 111 | 112 | ## Limitations 113 | 114 | ### Case handlers type variance 115 | 116 | ~~Avoid explicitly setting argument type in `caseWhen()` handler function, let type inferred by compiler. 117 | You may set more specific type, but check will bring you more general one and compiler will not fail. 118 | This is caused by TypeScript [Function Parameter Bivariance](https://www.typescriptlang.org/docs/handbook/type-compatibility.html) 119 | _feature_.~~ 120 | 121 | __UPD__: Typescript v2.6 brings `--strictFunctionTypes` compiler option and if it's on, for this code: 122 | 123 | ```typescript 124 | match(8, caseWhen(isNumber, (n: 10) => "n is 10")) 125 | ``` 126 | 127 | you will now get this error: 128 | 129 | ``` 130 | error TS2345: Argument of type '8' is not assignable to parameter of type '10'. 131 | 132 | match(8, caseWhen(isNumber, (n: 10) => "n is 10")) 133 | ~ 134 | ``` 135 | 136 | ### Use `caseDefault` at the end 137 | 138 | ~~`match` will execute all cases as provided, so first matching will return, 139 | use `caseDefault`, `caseAny` last.~~ 140 | 141 | New match DSL introduced in `typematcher@0.8.0` brought compile-time exhaustivity checking, so this code: 142 | 143 | ```typescript 144 | const x: "ten" | "twenty" = match(8 as any, 145 | caseWhen(isString, () => "ten") 146 | ) 147 | ``` 148 | 149 | will fail at compile time with: 150 | 151 | ``` 152 | error TS2322: Type 'string' is not assignable to type '"ten" | "twenty"'. 153 | 154 | const x: "ten" | "twenty" = match(8 as any, 155 | ~ 156 | ``` 157 | 158 | But you still have to handle default case when `any` result type is expected (which is highly not recommended), otherwise it may fail with `No match` error at runtime. 159 | 160 | ```typescript 161 | const x: any = match(8 as any, 162 | caseWhen(isString, () => "ten"). 163 | caseDefault(() => "twenty") 164 | ) 165 | ``` 166 | 167 | ## Contribute 168 | 169 | > Perfection is Achieved Not When There Is Nothing More to Add, 170 | > But When There Is Nothing Left to Take Away 171 | 172 | Fork, Contribute, Push, Create pull request, Thanks. 173 | 174 | ## Documentation 175 | 176 | Check latest sources on github: https://github.com/lostintime/node-typematcher. 177 | 178 | [Pattern matching for typescript](https://lostintimedev.com/2017/09/06/typematcher-pattern-matching-library-for-typescript.html) blog post. 179 | 180 | Funfix binding: [https://github.com/lostintime/node-typematcher-funfix](https://github.com/lostintime/node-typematcher-funfix). 181 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typematcher", 3 | "version": "0.10.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/chai": { 8 | "version": "4.1.4", 9 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.4.tgz", 10 | "integrity": "sha512-h6+VEw2Vr3ORiFCyyJmcho2zALnUq9cvdB/IO8Xs9itrJVCenC7o26A6+m7D0ihTTr65eS259H5/Ghl/VjYs6g==", 11 | "dev": true 12 | }, 13 | "@types/mocha": { 14 | "version": "5.2.5", 15 | "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz", 16 | "integrity": "sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww==", 17 | "dev": true 18 | }, 19 | "ansi-regex": { 20 | "version": "2.1.1", 21 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 22 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", 23 | "dev": true 24 | }, 25 | "ansi-styles": { 26 | "version": "2.2.1", 27 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", 28 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", 29 | "dev": true 30 | }, 31 | "argparse": { 32 | "version": "1.0.10", 33 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 34 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 35 | "dev": true, 36 | "requires": { 37 | "sprintf-js": "~1.0.2" 38 | } 39 | }, 40 | "assertion-error": { 41 | "version": "1.0.2", 42 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", 43 | "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", 44 | "dev": true 45 | }, 46 | "babel-code-frame": { 47 | "version": "6.26.0", 48 | "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", 49 | "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", 50 | "dev": true, 51 | "requires": { 52 | "chalk": "^1.1.3", 53 | "esutils": "^2.0.2", 54 | "js-tokens": "^3.0.2" 55 | }, 56 | "dependencies": { 57 | "chalk": { 58 | "version": "1.1.3", 59 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", 60 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", 61 | "dev": true, 62 | "requires": { 63 | "ansi-styles": "^2.2.1", 64 | "escape-string-regexp": "^1.0.2", 65 | "has-ansi": "^2.0.0", 66 | "strip-ansi": "^3.0.0", 67 | "supports-color": "^2.0.0" 68 | } 69 | }, 70 | "supports-color": { 71 | "version": "2.0.0", 72 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", 73 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", 74 | "dev": true 75 | } 76 | } 77 | }, 78 | "balanced-match": { 79 | "version": "1.0.0", 80 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 81 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 82 | "dev": true 83 | }, 84 | "brace-expansion": { 85 | "version": "1.1.11", 86 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 87 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 88 | "dev": true, 89 | "requires": { 90 | "balanced-match": "^1.0.0", 91 | "concat-map": "0.0.1" 92 | } 93 | }, 94 | "browser-stdout": { 95 | "version": "1.3.1", 96 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 97 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 98 | "dev": true 99 | }, 100 | "builtin-modules": { 101 | "version": "1.1.1", 102 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", 103 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", 104 | "dev": true 105 | }, 106 | "chai": { 107 | "version": "4.1.2", 108 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", 109 | "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", 110 | "dev": true, 111 | "requires": { 112 | "assertion-error": "^1.0.1", 113 | "check-error": "^1.0.1", 114 | "deep-eql": "^3.0.0", 115 | "get-func-name": "^2.0.0", 116 | "pathval": "^1.0.0", 117 | "type-detect": "^4.0.0" 118 | } 119 | }, 120 | "chalk": { 121 | "version": "2.4.1", 122 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", 123 | "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", 124 | "dev": true, 125 | "requires": { 126 | "ansi-styles": "^3.2.1", 127 | "escape-string-regexp": "^1.0.5", 128 | "supports-color": "^5.3.0" 129 | }, 130 | "dependencies": { 131 | "ansi-styles": { 132 | "version": "3.2.1", 133 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 134 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 135 | "dev": true, 136 | "requires": { 137 | "color-convert": "^1.9.0" 138 | } 139 | } 140 | } 141 | }, 142 | "check-error": { 143 | "version": "1.0.2", 144 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 145 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 146 | "dev": true 147 | }, 148 | "color-convert": { 149 | "version": "1.9.2", 150 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz", 151 | "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==", 152 | "dev": true, 153 | "requires": { 154 | "color-name": "1.1.1" 155 | } 156 | }, 157 | "color-name": { 158 | "version": "1.1.1", 159 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz", 160 | "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=", 161 | "dev": true 162 | }, 163 | "commander": { 164 | "version": "2.15.1", 165 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", 166 | "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", 167 | "dev": true 168 | }, 169 | "concat-map": { 170 | "version": "0.0.1", 171 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 172 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 173 | "dev": true 174 | }, 175 | "debug": { 176 | "version": "3.1.0", 177 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 178 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 179 | "dev": true, 180 | "requires": { 181 | "ms": "2.0.0" 182 | } 183 | }, 184 | "deep-eql": { 185 | "version": "3.0.1", 186 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 187 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 188 | "dev": true, 189 | "requires": { 190 | "type-detect": "^4.0.0" 191 | } 192 | }, 193 | "diff": { 194 | "version": "3.5.0", 195 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", 196 | "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", 197 | "dev": true 198 | }, 199 | "doctrine": { 200 | "version": "0.7.2", 201 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz", 202 | "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=", 203 | "dev": true, 204 | "requires": { 205 | "esutils": "^1.1.6", 206 | "isarray": "0.0.1" 207 | }, 208 | "dependencies": { 209 | "esutils": { 210 | "version": "1.1.6", 211 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz", 212 | "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=", 213 | "dev": true 214 | } 215 | } 216 | }, 217 | "escape-string-regexp": { 218 | "version": "1.0.5", 219 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 220 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 221 | "dev": true 222 | }, 223 | "esprima": { 224 | "version": "4.0.1", 225 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 226 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 227 | "dev": true 228 | }, 229 | "esutils": { 230 | "version": "2.0.2", 231 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 232 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", 233 | "dev": true 234 | }, 235 | "fs.realpath": { 236 | "version": "1.0.0", 237 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 238 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 239 | "dev": true 240 | }, 241 | "get-func-name": { 242 | "version": "2.0.0", 243 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 244 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 245 | "dev": true 246 | }, 247 | "glob": { 248 | "version": "7.1.2", 249 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 250 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 251 | "dev": true, 252 | "requires": { 253 | "fs.realpath": "^1.0.0", 254 | "inflight": "^1.0.4", 255 | "inherits": "2", 256 | "minimatch": "^3.0.4", 257 | "once": "^1.3.0", 258 | "path-is-absolute": "^1.0.0" 259 | } 260 | }, 261 | "growl": { 262 | "version": "1.10.5", 263 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 264 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 265 | "dev": true 266 | }, 267 | "has-ansi": { 268 | "version": "2.0.0", 269 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", 270 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", 271 | "dev": true, 272 | "requires": { 273 | "ansi-regex": "^2.0.0" 274 | } 275 | }, 276 | "has-flag": { 277 | "version": "3.0.0", 278 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 279 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 280 | "dev": true 281 | }, 282 | "he": { 283 | "version": "1.1.1", 284 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 285 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 286 | "dev": true 287 | }, 288 | "inflight": { 289 | "version": "1.0.6", 290 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 291 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 292 | "dev": true, 293 | "requires": { 294 | "once": "^1.3.0", 295 | "wrappy": "1" 296 | } 297 | }, 298 | "inherits": { 299 | "version": "2.0.3", 300 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 301 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 302 | "dev": true 303 | }, 304 | "isarray": { 305 | "version": "0.0.1", 306 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 307 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", 308 | "dev": true 309 | }, 310 | "js-tokens": { 311 | "version": "3.0.2", 312 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", 313 | "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", 314 | "dev": true 315 | }, 316 | "js-yaml": { 317 | "version": "3.12.0", 318 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz", 319 | "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==", 320 | "dev": true, 321 | "requires": { 322 | "argparse": "^1.0.7", 323 | "esprima": "^4.0.0" 324 | } 325 | }, 326 | "minimatch": { 327 | "version": "3.0.4", 328 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 329 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 330 | "dev": true, 331 | "requires": { 332 | "brace-expansion": "^1.1.7" 333 | } 334 | }, 335 | "minimist": { 336 | "version": "0.0.8", 337 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 338 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 339 | "dev": true 340 | }, 341 | "mkdirp": { 342 | "version": "0.5.1", 343 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 344 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 345 | "dev": true, 346 | "requires": { 347 | "minimist": "0.0.8" 348 | } 349 | }, 350 | "mocha": { 351 | "version": "5.2.0", 352 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", 353 | "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", 354 | "dev": true, 355 | "requires": { 356 | "browser-stdout": "1.3.1", 357 | "commander": "2.15.1", 358 | "debug": "3.1.0", 359 | "diff": "3.5.0", 360 | "escape-string-regexp": "1.0.5", 361 | "glob": "7.1.2", 362 | "growl": "1.10.5", 363 | "he": "1.1.1", 364 | "minimatch": "3.0.4", 365 | "mkdirp": "0.5.1", 366 | "supports-color": "5.4.0" 367 | } 368 | }, 369 | "ms": { 370 | "version": "2.0.0", 371 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 372 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 373 | "dev": true 374 | }, 375 | "once": { 376 | "version": "1.4.0", 377 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 378 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 379 | "dev": true, 380 | "requires": { 381 | "wrappy": "1" 382 | } 383 | }, 384 | "path-is-absolute": { 385 | "version": "1.0.1", 386 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 387 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 388 | "dev": true 389 | }, 390 | "path-parse": { 391 | "version": "1.0.6", 392 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 393 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 394 | "dev": true 395 | }, 396 | "pathval": { 397 | "version": "1.1.0", 398 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 399 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", 400 | "dev": true 401 | }, 402 | "resolve": { 403 | "version": "1.8.1", 404 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz", 405 | "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==", 406 | "dev": true, 407 | "requires": { 408 | "path-parse": "^1.0.5" 409 | } 410 | }, 411 | "semver": { 412 | "version": "5.5.0", 413 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", 414 | "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", 415 | "dev": true 416 | }, 417 | "sprintf-js": { 418 | "version": "1.0.3", 419 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 420 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 421 | "dev": true 422 | }, 423 | "strip-ansi": { 424 | "version": "3.0.1", 425 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 426 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 427 | "dev": true, 428 | "requires": { 429 | "ansi-regex": "^2.0.0" 430 | } 431 | }, 432 | "supports-color": { 433 | "version": "5.4.0", 434 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", 435 | "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", 436 | "dev": true, 437 | "requires": { 438 | "has-flag": "^3.0.0" 439 | } 440 | }, 441 | "tslib": { 442 | "version": "1.9.3", 443 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", 444 | "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", 445 | "dev": true 446 | }, 447 | "tslint": { 448 | "version": "5.11.0", 449 | "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.11.0.tgz", 450 | "integrity": "sha1-mPMMAurjzecAYgHkwzywi0hYHu0=", 451 | "dev": true, 452 | "requires": { 453 | "babel-code-frame": "^6.22.0", 454 | "builtin-modules": "^1.1.1", 455 | "chalk": "^2.3.0", 456 | "commander": "^2.12.1", 457 | "diff": "^3.2.0", 458 | "glob": "^7.1.1", 459 | "js-yaml": "^3.7.0", 460 | "minimatch": "^3.0.4", 461 | "resolve": "^1.3.2", 462 | "semver": "^5.3.0", 463 | "tslib": "^1.8.0", 464 | "tsutils": "^2.27.2" 465 | } 466 | }, 467 | "tslint-config-standard": { 468 | "version": "7.1.0", 469 | "resolved": "https://registry.npmjs.org/tslint-config-standard/-/tslint-config-standard-7.1.0.tgz", 470 | "integrity": "sha512-cETzxZcEQ1RKjwtEScGryAtqwiRFc55xBxhZP6bePyOfXmo6i1/QKQrTgFKBiM4FjCvcqTjJq20/KGrh+TzTfQ==", 471 | "dev": true, 472 | "requires": { 473 | "tslint-eslint-rules": "^5.3.1" 474 | } 475 | }, 476 | "tslint-eslint-rules": { 477 | "version": "5.4.0", 478 | "resolved": "https://registry.npmjs.org/tslint-eslint-rules/-/tslint-eslint-rules-5.4.0.tgz", 479 | "integrity": "sha512-WlSXE+J2vY/VPgIcqQuijMQiel+UtmXS+4nvK4ZzlDiqBfXse8FAvkNnTcYhnQyOTW5KFM+uRRGXxYhFpuBc6w==", 480 | "dev": true, 481 | "requires": { 482 | "doctrine": "0.7.2", 483 | "tslib": "1.9.0", 484 | "tsutils": "^3.0.0" 485 | }, 486 | "dependencies": { 487 | "tslib": { 488 | "version": "1.9.0", 489 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", 490 | "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", 491 | "dev": true 492 | }, 493 | "tsutils": { 494 | "version": "3.0.0", 495 | "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.0.0.tgz", 496 | "integrity": "sha512-LjHBWR0vWAUHWdIAoTjoqi56Kz+FDKBgVEuL+gVPG/Pv7QW5IdaDDeK9Txlr6U0Cmckp5EgCIq1T25qe3J6hyw==", 497 | "dev": true, 498 | "requires": { 499 | "tslib": "^1.8.1" 500 | } 501 | } 502 | } 503 | }, 504 | "tsutils": { 505 | "version": "2.29.0", 506 | "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", 507 | "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", 508 | "dev": true, 509 | "requires": { 510 | "tslib": "^1.8.1" 511 | } 512 | }, 513 | "type-detect": { 514 | "version": "4.0.5", 515 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.5.tgz", 516 | "integrity": "sha512-N9IvkQslUGYGC24RkJk1ba99foK6TkwC2FHAEBlQFBP0RxQZS8ZpJuAZcwiY/w9ZJHFQb1aOXBI60OdxhTrwEQ==", 517 | "dev": true 518 | }, 519 | "typescript": { 520 | "version": "3.0.3", 521 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.3.tgz", 522 | "integrity": "sha512-kk80vLW9iGtjMnIv11qyxLqZm20UklzuR2tL0QAnDIygIUIemcZMxlMWudl9OOt76H3ntVzcTiddQ1/pAAJMYg==", 523 | "dev": true 524 | }, 525 | "wrappy": { 526 | "version": "1.0.2", 527 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 528 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 529 | "dev": true 530 | } 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typematcher", 3 | "author": "lostintime", 4 | "version": "0.10.2", 5 | "license": "MIT", 6 | "description": "Type matching library for TypeScript", 7 | "main": "dist/lib/index.js", 8 | "types": "dist/lib/index.d.ts", 9 | "keywords": [ 10 | "TypeScript", 11 | "typematch", 12 | "pattern matching", 13 | "type safe" 14 | ], 15 | "bugs": { 16 | "url": "https://github.com/lostintime/node-typematcher/issues" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/lostintime/node-typematcher.git" 21 | }, 22 | "scripts": { 23 | "test": "tsc && mocha \"dist/test/\"", 24 | "lint": "tslint --project tsconfig.json", 25 | "clean": "rm -R dist/", 26 | "prebuild": "npm run clean", 27 | "build": "tsc", 28 | "build:watch": "tsc -w" 29 | }, 30 | "devDependencies": { 31 | "@types/chai": "^4.1.4", 32 | "@types/mocha": "^5.2.5", 33 | "chai": "^4.1.2", 34 | "mocha": "^5.2.0", 35 | "tslint": "^5.11.0", 36 | "tslint-config-standard": "^7.1.0", 37 | "tslint-eslint-rules": "^5.4.0", 38 | "typescript": "^3.0.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ## `0.7.0`: 5 | 6 | Version is backward incompatible with previous releases (latest is `0.6.1`) 7 | 8 | * Fully refactored matching dsl: added exhaustive input value type checks (backward incompatible changes); 9 | * `isValue` type matcher removed because value check does not fully cover infered type, ex. `isValue("hello")` will pass only `"hello"` values string but infered type is `string` 10 | 11 | ## `0.6.1` 12 | 13 | * Dev Dependencies updates (typescript `2.7`) 14 | 15 | ## `0.5.0`, `0.6.0` 16 | 17 | * Adds/Updates `refined(TypeMatcher)((A) => boolean, T)` functon -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 by TypeMatcher developers 3 | * 4 | * 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: 5 | * 6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | * 8 | * 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. 9 | */ 10 | 11 | export * from "./matchers" 12 | export * from "./match-dsl" 13 | -------------------------------------------------------------------------------- /src/lib/match-dsl.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 by TypeMatcher developers 3 | * 4 | * 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: 5 | * 6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | * 8 | * 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. 9 | */ 10 | 11 | import { TypeMatcher } from "./matchers" 12 | 13 | /** 14 | * Cases for partial matches 15 | * @private 16 | */ 17 | export interface PartialMatchCase { 18 | /** 19 | * partMap - maps value A to R on match, or returns def expression value 20 | * @private 21 | * @param val value to map 22 | * @param def default value expresson, for match miss 23 | */ 24 | partMap(val: A, def: () => R): R 25 | } 26 | 27 | /** 28 | * MatchCase is basically a function between category A and R 29 | */ 30 | export interface MatchCase { 31 | /** 32 | * Cases handler no default handler, depends on implementation 33 | */ 34 | map(val: A): R 35 | } 36 | 37 | /** 38 | * SingleCase - map type A to R or fail with an error 39 | * 40 | * ``` 41 | * caseWhen(isOne, id). // <- SingleCase 42 | * caseWhen(isTwo, id). // <- DisjunctionCase, executes previous case and falls back to itself 43 | * caseWhen(isThree, id). // <- one more DisjunctionCase 44 | * caseDefault(() => c) // <- DefaultCase - adds default value expression 45 | * ``` 46 | */ 47 | export class SingleCase implements MatchCase, PartialMatchCase { 48 | constructor(private readonly match: TypeMatcher, 49 | private readonly handle: (val: A) => R) {} 50 | 51 | partMap(val: A, def: () => R): R { 52 | if (this.match(val)) { 53 | return this.handle(val) 54 | } 55 | 56 | return def() 57 | } 58 | 59 | map(val: A): R { 60 | return this.partMap(val, () => { 61 | throw new Error("No match") 62 | }) 63 | } 64 | 65 | /** 66 | * Add a disjunction case 67 | */ 68 | caseWhen(match: TypeMatcher, handle: (val: B) => R2): DisjunctionCase { 69 | return new DisjunctionCase( 70 | new SingleCase(this.match, this.handle), // head case - handled first 71 | new SingleCase(match, handle) // tail case - handled last 72 | ) 73 | } 74 | 75 | caseDefault(def: () => R2): DefaultCase { 76 | return new DefaultCase(this, def) 77 | } 78 | } 79 | 80 | /** 81 | * DisjunctionCase - composes tail and head cases 82 | * 83 | * @param tailCases - chain of previous cases, tail executes first 84 | * @param headCase - head case (referenced), to be executed last in the chain 85 | */ 86 | export class DisjunctionCase implements MatchCase, PartialMatchCase { 87 | constructor( 88 | private readonly tailCases: PartialMatchCase, 89 | private readonly headCase: PartialMatchCase) { 90 | } 91 | 92 | partMap(val: A | B, def: () => R | R2): R | R2 { 93 | return this.tailCases.partMap(val, (): R | R2 => { 94 | return this.headCase.partMap(val, def) 95 | }) 96 | } 97 | 98 | /** 99 | * When called directly on DisjunctionCase - will throw an error if input value not covered 100 | */ 101 | map(val: A | B): R | R2 { 102 | return this.partMap(val, () => { 103 | throw new Error("No match") 104 | }) 105 | } 106 | 107 | caseWhen(match: TypeMatcher, handle: (val: C) => R3): DisjunctionCase { 108 | return new DisjunctionCase( 109 | this, 110 | new SingleCase(match, handle) 111 | ) 112 | } 113 | 114 | caseDefault(def: () => R3): DefaultCase { 115 | return new DefaultCase(this, def) 116 | } 117 | } 118 | 119 | /** 120 | * Last case in a chain of cases (default) 121 | */ 122 | export class DefaultCase implements MatchCase { 123 | constructor( 124 | private readonly tailCase: PartialMatchCase, 125 | private readonly def: () => R) { 126 | } 127 | 128 | map(val: unknown): R { 129 | return this.tailCase.partMap(val, this.def) 130 | } 131 | } 132 | 133 | /** 134 | * Case always evaluates to given expression result 135 | */ 136 | export class EvalCase implements MatchCase { 137 | constructor(private readonly val: () => R) {} 138 | 139 | map(val: unknown): R { 140 | return this.val() 141 | } 142 | } 143 | 144 | /** 145 | * Case handler for type A 146 | * 147 | * @param match matcher used to verify input value 148 | * @param map map function 149 | */ 150 | export function caseWhen(match: TypeMatcher, map: (val: A) => R): SingleCase { 151 | return new SingleCase(match, map) 152 | } 153 | 154 | /** 155 | * Create new default case handler - matches any type 156 | * 157 | * @param val default value expression 158 | */ 159 | export function caseDefault(val: () => R): EvalCase { 160 | return new EvalCase(val) 161 | } 162 | 163 | /** 164 | * Pattern Matching syntax sugar 165 | * 166 | * Equivalent to Case.map(val) 167 | * @param val input value to match 168 | * @param cases match cases 169 | */ 170 | export function match(val: I, cases: MatchCase): R { 171 | return cases.map(val) 172 | } 173 | 174 | /** 175 | * Builds an unary function C => R using a MatchCase instance (syntactic sugar over MatchCase.map to trick compiler) 176 | * 177 | * It is useful to define type encoders/decoders, ex: 178 | * ```typescript 179 | * type Name = { 180 | * firstName: string 181 | * lastName = string 182 | * } 183 | * 184 | * const Name: (val: unknown) => Name | null = matcher( 185 | * caseWhen(isName, (_): Name => _). 186 | * caseDefault(() => null) 187 | * ) 188 | * 189 | * const name: Name | null = Name({}) 190 | * ``` 191 | * 192 | * @param cases match cases 193 | */ 194 | export function matcher(cases: MatchCase): (val: C) => R { 195 | return val => cases.map(val) 196 | } 197 | -------------------------------------------------------------------------------- /src/lib/matchers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 by TypeMatcher developers 3 | * 4 | * 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: 5 | * 6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | * 8 | * 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. 9 | */ 10 | 11 | /** 12 | * TypeMatcher is a function returning true if input val matches given type T 13 | */ 14 | export type TypeMatcher = (val: unknown) => val is T 15 | 16 | /** 17 | * Object fields type matchers structure 18 | */ 19 | export type FieldsMatcher = { [P in keyof T]-?: TypeMatcher } 20 | 21 | /** 22 | * Simple type alias to mark refined types, do not use _tag property! 23 | */ 24 | export type Refined = U & { readonly __tag: T } 25 | 26 | /** 27 | * Type alias for errors 28 | */ 29 | export type Throwable = Error | Object 30 | 31 | /** 32 | * Match any of input values 33 | */ 34 | export function isAny(val: unknown): val is any { 35 | return true 36 | } 37 | 38 | /** 39 | * Match any (unknown) input values 40 | */ 41 | export function isUnknown(val: unknown): val is unknown { 42 | return true 43 | } 44 | 45 | /** 46 | * Match none of input values 47 | */ 48 | export function isNever(val: unknown): val is never { 49 | return false 50 | } 51 | 52 | /** 53 | * Match string values 54 | */ 55 | export function isString(val: unknown): val is string { 56 | // Honestly stolen from https://github.com/lodash/lodash/blob/master/isString.js 57 | const type = typeof val 58 | return type === "string" || (type === "object" && val != null && !Array.isArray(val) && Object.prototype.toString.call(val) === "[object String]") 59 | } 60 | 61 | /** 62 | * Match number values 63 | */ 64 | export function isNumber(value: unknown): value is number { 65 | // Honestly stolen from https://github.com/lodash/lodash/blob/master/isNumber.js 66 | return typeof value === "number" || 67 | (typeof value === "object" && value !== null && Object.prototype.toString.call(value) === "[object Number]") 68 | } 69 | 70 | /** 71 | * Match number values but not NaN or Infinite 72 | */ 73 | export function isFiniteNumber(value: unknown): value is number { 74 | return isNumber(value) && !isNaN(value) && isFinite(value) 75 | } 76 | 77 | /** 78 | * Match boolean values 79 | */ 80 | export function isBoolean(value: unknown): value is boolean { 81 | // Honestly stolen from https://github.com/lodash/lodash/blob/master/isBoolean.js 82 | return value === true || value === false || 83 | (typeof value === "object" && value !== null && Object.prototype.toString.call(value) === "[object Boolean]") 84 | } 85 | 86 | /** 87 | * Match object values 88 | */ 89 | export function isObject(value: unknown): value is object { 90 | // Honestly stolen from https://github.com/lodash/lodash/blob/master/isObject.js 91 | const type = typeof value 92 | return value != null && (type === "object" || type === "function") 93 | } 94 | 95 | /** 96 | * Check value built with given constructor function 97 | */ 98 | export function isInstanceOf(fnCtor: new (...args: unknown[]) => T): TypeMatcher { 99 | return function value(val: unknown): val is T { 100 | return val instanceof fnCtor 101 | } 102 | } 103 | 104 | /** 105 | * Match input value is array of T using given matcher 106 | * isArrayOf(isString)([1]) => false 107 | * isArrayOf(isNumber)([1]) => true 108 | * isArrayOf(isString)(["one", 1]) => false 109 | */ 110 | export function isArrayOf(matcher: TypeMatcher): TypeMatcher> { 111 | return function value(val: unknown): val is Array { 112 | if (Array.isArray(val)) { 113 | for (const item of val) { 114 | if (!matcher(item)) { 115 | // one of items doesn't match, fail fast 116 | return false 117 | } 118 | } 119 | 120 | return true 121 | } 122 | 123 | return false 124 | } 125 | } 126 | 127 | /** 128 | * Checks value equality 129 | * 130 | * WARNING: always set type hint explicitly, otherwise - exhaustive checks will not work 131 | */ 132 | export function isLiteral(expected: T): TypeMatcher { 133 | return function value(val: unknown): val is T { 134 | return expected === val 135 | } 136 | } 137 | 138 | /** 139 | * Match null 140 | */ 141 | export function isNull(val: unknown): val is null { 142 | return val === null 143 | } 144 | 145 | /** 146 | * Match undefined 147 | */ 148 | export function isUndefined(val: unknown): val is undefined { 149 | return val === undefined 150 | } 151 | 152 | const isMissingF = isEither(isNull, isUndefined) 153 | 154 | /** 155 | * Match null | undefined 156 | */ 157 | export function isMissing(val: unknown): val is null | undefined { 158 | return isMissingF(val) 159 | } 160 | 161 | /** 162 | * Match object fields by given matchers 163 | * hasFields({id: isNumber})({id: "aloha"}) => false 164 | */ 165 | export function hasFields(matcher: FieldsMatcher): TypeMatcher { 166 | return function value(val: unknown): val is T { 167 | if (isObject(val)) { 168 | for (const pKey in matcher) { 169 | const v = val.hasOwnProperty(pKey) ? (val as any)[pKey] : undefined 170 | 171 | if (!matcher[pKey](v)) { 172 | // one of required fields doesn't match, fail fast 173 | return false 174 | } 175 | } 176 | 177 | return true 178 | } 179 | 180 | return false 181 | } 182 | } 183 | 184 | export function hasOptionalFields(matcher: FieldsMatcher): TypeMatcher> { 185 | return function value(val: unknown): val is T { 186 | if (isObject(val)) { 187 | for (const pKey in matcher) { 188 | const v = val.hasOwnProperty(pKey) ? (val as any)[pKey] : undefined 189 | 190 | if (v !== undefined && !matcher[pKey](v)) { 191 | // one of required fields doesn't match, fail fast 192 | return false 193 | } 194 | } 195 | 196 | return true 197 | } 198 | 199 | return false 200 | } 201 | } 202 | 203 | export type ObjectMapOf = {[K in string]: T} 204 | 205 | export function isObjectMapOf(matcher: TypeMatcher): TypeMatcher> { 206 | return function value(val: unknown): val is ObjectMapOf { 207 | if (isObject(val)) { 208 | for (const pKey in val) { 209 | if (!val.hasOwnProperty(pKey) || !matcher((val as any)[pKey])) { 210 | return false 211 | } 212 | } 213 | 214 | return true 215 | } 216 | 217 | return false 218 | } 219 | } 220 | 221 | /** 222 | * Given array of matchers, and array - match every value using matcher from same position 223 | * this is internally used for tuples 224 | */ 225 | function isExactArray(matcher: Array>, val: unknown): val is Array { 226 | if (Array.isArray(val) && val.length === matcher.length) { 227 | for (const k in matcher) { 228 | if (!matcher[k](val[k])) { 229 | return false 230 | } 231 | } 232 | return true 233 | } 234 | 235 | return false 236 | 237 | } 238 | 239 | export function isTuple1(a: TypeMatcher): TypeMatcher<[A]> { 240 | return function value(val: unknown): val is [A] { 241 | return isExactArray([a], val) 242 | } 243 | } 244 | 245 | export function isTuple2(a: TypeMatcher, b: TypeMatcher): TypeMatcher<[A, B]> { 246 | return function value(val: unknown): val is [A, B] { 247 | return isExactArray([a, b], val) 248 | } 249 | } 250 | 251 | export function isTuple3(a: TypeMatcher, b: TypeMatcher, c: TypeMatcher): TypeMatcher<[A, B, C]> { 252 | return function value(val: unknown): val is [A, B, C] { 253 | return isExactArray([a, b, c], val) 254 | } 255 | } 256 | 257 | export function isTuple4(a: TypeMatcher, b: TypeMatcher, c: TypeMatcher, d: TypeMatcher): TypeMatcher<[A, B, C, D]> { 258 | return function value(val: unknown): val is [A, B, C, D] { 259 | return isExactArray([a, b, c, d], val) 260 | } 261 | } 262 | 263 | export function isTuple5(a: TypeMatcher, b: TypeMatcher, c: TypeMatcher, d: TypeMatcher, e: TypeMatcher): TypeMatcher<[A, B, C, D, E]> { 264 | return function value(val: unknown): val is [A, B, C, D, E] { 265 | return isExactArray([a, b, c, d, e], val) 266 | } 267 | } 268 | 269 | export function isTuple6(a: TypeMatcher, b: TypeMatcher, c: TypeMatcher, d: TypeMatcher, e: TypeMatcher, f: TypeMatcher): TypeMatcher<[A, B, C, D, E, F]> { 270 | return function value(val: unknown): val is [A, B, C, D, E, F] { 271 | return isExactArray([a, b, c, d, e, f], val) 272 | } 273 | } 274 | 275 | export function isTuple7(a: TypeMatcher, b: TypeMatcher, c: TypeMatcher, d: TypeMatcher, e: TypeMatcher, f: TypeMatcher, g: TypeMatcher): TypeMatcher<[A, B, C, D, E, F, G]> { 276 | return function value(val: unknown): val is [A, B, C, D, E, F, G] { 277 | return isExactArray([a, b, c, d, e, f, g], val) 278 | } 279 | } 280 | 281 | export function isTuple8(a: TypeMatcher, b: TypeMatcher, c: TypeMatcher, d: TypeMatcher, e: TypeMatcher, f: TypeMatcher, g: TypeMatcher, h: TypeMatcher): TypeMatcher<[A, B, C, D, E, F, G, H]> { 282 | return function value(val: unknown): val is [A, B, C, D, E, F, G, H] { 283 | return isExactArray([a, b, c, d, e, f, g, h], val) 284 | } 285 | } 286 | 287 | export function isTuple9(a: TypeMatcher, b: TypeMatcher, c: TypeMatcher, d: TypeMatcher, e: TypeMatcher, f: TypeMatcher, g: TypeMatcher, h: TypeMatcher, i: TypeMatcher): TypeMatcher<[A, B, C, D, E, F, G, H, I]> { 288 | return function value(val: unknown): val is [A, B, C, D, E, F, G, H, I] { 289 | return isExactArray([a, b, c, d, e, f, g, h, i], val) 290 | } 291 | } 292 | 293 | export function isTuple10(a: TypeMatcher, b: TypeMatcher, c: TypeMatcher, d: TypeMatcher, e: TypeMatcher, f: TypeMatcher, g: TypeMatcher, h: TypeMatcher, i: TypeMatcher, j: TypeMatcher): TypeMatcher<[A, B, C, D, E, F, G, H, I, J]> { 294 | return function value(val: unknown): val is [A, B, C, D, E, F, G, H, I, J] { 295 | return isExactArray([a, b, c, d, e, f, g, h, i, j], val) 296 | } 297 | } 298 | 299 | /** 300 | * Builds new matcher for types matching both: matcher1 and matcher2 301 | */ 302 | export function isBoth(matcher1: TypeMatcher, matcher2: TypeMatcher): TypeMatcher { 303 | return function value(val: unknown): val is A & B { 304 | return matcher1(val) && matcher2(val) 305 | } 306 | } 307 | 308 | /** 309 | * Builds new matcher for types matching any of matcher1 or matcher2 310 | */ 311 | export function isEither(matcher1: TypeMatcher, matcher2: TypeMatcher): TypeMatcher { 312 | return function value(val: unknown): val is A | B { 313 | return matcher1(val) || matcher2(val) 314 | } 315 | } 316 | 317 | /** 318 | * Builds new matcher for value which may be undefined 319 | */ 320 | export function isOptional(matcher: TypeMatcher): TypeMatcher { 321 | return isEither(isUndefined, matcher) 322 | } 323 | 324 | /** 325 | * Builds new matcher for value which may be null 326 | */ 327 | export function isNullable(matcher: TypeMatcher): TypeMatcher { 328 | return isEither(isNull, matcher) 329 | } 330 | 331 | /** 332 | * Build refined type matcher, ex: 333 | * const isPositive: TypeMatcher> = refined(isFiniteNumber)(_ => _ > 0, "Positive") 334 | * isPositive(1) === true 335 | * isPositive(0) === false 336 | * isPositive(-1) === false 337 | */ 338 | export function refined(m: TypeMatcher): (fn: (_: U) => boolean, tag: T) => TypeMatcher> { 339 | return (fn: (_: U) => boolean, tag: T): TypeMatcher> => { 340 | const refn = fn as ((val: U) => val is (Refined)) 341 | 342 | return (_: unknown): _ is Refined => m(_) && refn(_) 343 | } 344 | } 345 | 346 | /** 347 | * Builds new matcher which throws an error on miss 348 | * Can be used to provide more useful errors in combination with hasFields() and match() 349 | * As throwing exceptions is not "the best way" to controll program flow - use this only for critical cases (panics) 350 | * 351 | * ``` 352 | * const result = match({}, 353 | * caseWhen( 354 | * hasFields({ 355 | * title: failWith(new Error("Invalid title: string expected"))(isString), 356 | * description: failWith(new Error('Invalid description: string or undefined expected'))(isOptional(isString)), 357 | * }), 358 | * _ => _ 359 | * ) 360 | * ) 361 | * ``` 362 | * 363 | * @deprecated do not throw, use disjunction data types 364 | */ 365 | export function failWith(err: Throwable): (matcher: TypeMatcher) => TypeMatcher { 366 | return function on(matcher: TypeMatcher): TypeMatcher { 367 | return function value(val: unknown): val is T { 368 | if (matcher(val)) { 369 | return true 370 | } 371 | 372 | throw err 373 | } 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /src/test/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 by TypeMatcher developers 3 | * 4 | * 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: 5 | * 6 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | * 8 | * 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. 9 | */ 10 | 11 | import { expect } from "chai" 12 | import { 13 | TypeMatcher, Refined, hasFields, isLiteral, 14 | isAny, isArrayOf, isBoolean, isFiniteNumber, isMissing, isNever, isNull, isNumber, isObject, 15 | isString, isUndefined, isTuple1, isTuple2, isTuple3, isTuple4, isTuple5, isTuple6, isTuple7, 16 | isTuple8, isTuple9, isTuple10, isBoth, isEither, isOptional, isNullable, refined, match, caseWhen, 17 | caseDefault, failWith, isInstanceOf, isObjectMapOf, MatchCase, matcher, hasOptionalFields, 18 | isUnknown 19 | } from "../lib" 20 | 21 | describe("Matchers", () => { 22 | describe("isAny", () => { 23 | it("should match any input", () => { 24 | expect(isAny(false)).equals(true) 25 | expect(isAny(NaN)).equals(true) 26 | expect(isAny(Infinity)).equals(true) 27 | expect(isAny(10)).equals(true) 28 | expect(isAny(1.3)).equals(true) 29 | expect(isAny("string")).equals(true) 30 | expect(isAny({})).equals(true) 31 | expect(isAny([])).equals(true) 32 | expect(isAny(null)).equals(true) 33 | expect(isAny(undefined)).equals(true) 34 | }) 35 | }) 36 | 37 | describe("isUnknown", () => { 38 | it("should match any input", () => { 39 | expect(isUnknown(false)).equals(true) 40 | expect(isUnknown(NaN)).equals(true) 41 | expect(isUnknown(Infinity)).equals(true) 42 | expect(isUnknown(10)).equals(true) 43 | expect(isUnknown(1.3)).equals(true) 44 | expect(isUnknown("string")).equals(true) 45 | expect(isUnknown({})).equals(true) 46 | expect(isUnknown([])).equals(true) 47 | expect(isUnknown(null)).equals(true) 48 | expect(isUnknown(undefined)).equals(true) 49 | }) 50 | }) 51 | 52 | describe("isNever", () => { 53 | it("should never match any input", () => { 54 | expect(isNever(false)).equals(false) 55 | expect(isNever(NaN)).equals(false) 56 | expect(isNever(Infinity)).equals(false) 57 | expect(isNever(10)).equals(false) 58 | expect(isNever(1.3)).equals(false) 59 | expect(isNever("string")).equals(false) 60 | expect(isNever({})).equals(false) 61 | expect(isNever([])).equals(false) 62 | expect(isNever(null)).equals(false) 63 | expect(isNever(undefined)).equals(false) 64 | }) 65 | }) 66 | 67 | describe("isString", () => { 68 | it("should match string input", () => { 69 | expect(isString("aloha")).equals(true) 70 | expect(isString("aloha2")).equals(true) 71 | }) 72 | 73 | it("should match String object input", () => { 74 | expect(isString(new String(""))).equals(true) 75 | }) 76 | 77 | it("should not match other types", () => { 78 | expect(isString(false)).equals(false) 79 | expect(isString(NaN)).equals(false) 80 | expect(isString(Infinity)).equals(false) 81 | expect(isString(10)).equals(false) 82 | expect(isString(1.3)).equals(false) 83 | expect(isString({})).equals(false) 84 | expect(isString([])).equals(false) 85 | expect(isString(null)).equals(false) 86 | expect(isString(undefined)).equals(false) 87 | }) 88 | }) 89 | 90 | describe("isNumber", () => { 91 | it("should match number input", () => { 92 | expect(isNumber(10)).equals(true) 93 | expect(isNumber(3.4)).equals(true) 94 | expect(isNumber(-0.8843)).equals(true) 95 | }) 96 | 97 | it("should match NaN and Infinity", () => { 98 | expect(isNumber(NaN)).equals(true) 99 | expect(isNumber(Infinity)).equals(true) 100 | }) 101 | 102 | it("should not match other types", () => { 103 | expect(isNumber(false)).equals(false) 104 | expect(isNumber({})).equals(false) 105 | expect(isNumber(null)).equals(false) 106 | expect(isNumber("100")).equals(false) 107 | expect(isNumber(undefined)).equals(false) 108 | }) 109 | }) 110 | 111 | describe("isFiniteNumber", () => { 112 | it("should match number input", () => { 113 | expect(isFiniteNumber(10)).equals(true) 114 | expect(isFiniteNumber(3.4)).equals(true) 115 | expect(isFiniteNumber(-0.8843)).equals(true) 116 | }) 117 | 118 | it("should not match NaN and Infinity", () => { 119 | expect(isFiniteNumber(NaN)).equals(false) 120 | expect(isFiniteNumber(Infinity)).equals(false) 121 | }) 122 | 123 | it("should not match other types", () => { 124 | expect(isFiniteNumber(false)).equals(false) 125 | expect(isFiniteNumber({})).equals(false) 126 | expect(isFiniteNumber(null)).equals(false) 127 | expect(isFiniteNumber(undefined)).equals(false) 128 | }) 129 | }) 130 | 131 | describe("isBoolean", () => { 132 | it("should match boolean values", () => { 133 | expect(isBoolean(true)).equals(true) 134 | expect(isBoolean(false)).equals(true) 135 | }) 136 | 137 | it("should not match other types", () => { 138 | expect(isBoolean(NaN)).equals(false) 139 | expect(isBoolean(Infinity)).equals(false) 140 | expect(isBoolean(1)).equals(false) 141 | expect(isBoolean(0)).equals(false) 142 | expect(isBoolean(1.3)).equals(false) 143 | expect(isBoolean("true")).equals(false) 144 | expect(isBoolean({})).equals(false) 145 | expect(isBoolean([])).equals(false) 146 | expect(isBoolean(null)).equals(false) 147 | expect(isBoolean(undefined)).equals(false) 148 | }) 149 | }) 150 | 151 | describe("isObject", () => { 152 | it("should match any object", () => { 153 | expect(isObject({})).equals(true) 154 | expect(isObject([])).equals(true) 155 | expect(isObject(new String(""))).equals(true) 156 | expect(isObject(new Number(10))).equals(true) 157 | expect(isObject(new Boolean(true))).equals(true) 158 | expect(isObject(() => "")).equals(true) 159 | }) 160 | 161 | it("should not match other types", () => { 162 | expect(isObject(10)).equals(false) 163 | expect(isObject(0.2)).equals(false) 164 | expect(isObject("")).equals(false) 165 | expect(isObject(false)).equals(false) 166 | expect(isObject(NaN)).equals(false) 167 | expect(isObject(Infinity)).equals(false) 168 | expect(isObject(null)).equals(false) 169 | expect(isObject(undefined)).equals(false) 170 | }) 171 | }) 172 | 173 | describe("isInstanceOf", () => { 174 | it("will test instances created with provided constructors", () => { 175 | class A { 176 | } 177 | 178 | class B { 179 | } 180 | 181 | class C extends A { 182 | } 183 | 184 | const a = new A() 185 | const b = new B() 186 | const c = new C() 187 | 188 | expect(isInstanceOf(A)(a)).equals(true, "a is A") 189 | expect(isInstanceOf(B)(b)).equals(true, "b is B") 190 | expect(isInstanceOf(A)(b)).equals(false, "b is not A") 191 | expect(isInstanceOf(B)(a)).equals(false, "a is not B") 192 | expect(isInstanceOf(C)(c)).equals(true, "c is C") 193 | expect(isInstanceOf(A)(c)).equals(true, "c is A") 194 | }) 195 | }) 196 | 197 | describe("isArrayOf", () => { 198 | it("should match empty arrays", () => { 199 | expect(isArrayOf(isNumber)([])).equals(true) 200 | expect(isArrayOf(isFiniteNumber)([])).equals(true) 201 | expect(isArrayOf(isAny)([])).equals(true) 202 | expect(isArrayOf(isNever)([])).equals(true) 203 | expect(isArrayOf(isBoolean)([])).equals(true) 204 | expect(isArrayOf(isString)([])).equals(true) 205 | expect(isArrayOf(isObject)([])).equals(true) 206 | }) 207 | 208 | it("should match arrays with all values valid", () => { 209 | expect(isArrayOf(isNumber)([1, 2, 3, 5.6, NaN, Infinity])).equals(true) 210 | expect(isArrayOf(isString)(["one", "two", "three"])).equals(true) 211 | expect(isArrayOf(isBoolean)([true, new Boolean(false)])).equals(true) 212 | expect(isArrayOf(isArrayOf(isFiniteNumber))([[1], [2], [3]])).equals(true) 213 | }) 214 | 215 | it("should not match arrays containing invalid values", () => { 216 | expect(isArrayOf(isNumber)(["one", "two", 3])).equals(false) 217 | expect(isArrayOf(isFiniteNumber)([10, NaN, 20])).equals(false) 218 | expect(isArrayOf(isFiniteNumber)([12, 15, Infinity, 19])).equals(false) 219 | expect(isArrayOf(isArrayOf(isFiniteNumber))([[1], [2], [1, 2, "three"]])).equals(false) 220 | }) 221 | 222 | it("should not match other types", () => { 223 | expect(isArrayOf(isAny)(NaN)).equals(false) 224 | expect(isArrayOf(isAny)(Infinity)).equals(false) 225 | expect(isArrayOf(isAny)(1)).equals(false) 226 | expect(isArrayOf(isAny)(0)).equals(false) 227 | expect(isArrayOf(isAny)(1.3)).equals(false) 228 | expect(isArrayOf(isAny)("true")).equals(false) 229 | expect(isArrayOf(isAny)({})).equals(false) 230 | expect(isArrayOf(isAny)(null)).equals(false) 231 | expect(isArrayOf(isAny)(undefined)).equals(false) 232 | }) 233 | }) 234 | 235 | describe("isLiteral", () => { 236 | it("should exactly match values", () => { 237 | expect(isLiteral(10)(10)).equals(true) 238 | expect(isLiteral("one")("one")).equals(true) 239 | expect(isLiteral(true)(true)).equals(true) 240 | expect(isLiteral(Infinity)(Infinity)).equals(true) 241 | }) 242 | it("should not match values with different types", () => { 243 | expect(isLiteral("10")(10)).equals(false) 244 | expect(isLiteral(0)(false)).equals(false) 245 | expect(isLiteral(1)(true)).equals(false) 246 | expect(isLiteral(NaN)(NaN)).equals(false) 247 | }) 248 | it("should not match different values with same type", () => { 249 | expect(isLiteral("10")("20")).equals(false) 250 | expect(isLiteral(1)(1.1)).equals(false) 251 | expect(isLiteral(true)(false)).equals(false) 252 | }) 253 | }) 254 | 255 | describe("isNull", () => { 256 | it("should match for null", () => { 257 | expect(isNull(null)).equals(true) 258 | }) 259 | 260 | it("should not match for values other than null", () => { 261 | expect(isNull(NaN)).equals(false) 262 | expect(isNull(Infinity)).equals(false) 263 | expect(isNull(1)).equals(false) 264 | expect(isNull(0)).equals(false) 265 | expect(isNull(1.3)).equals(false) 266 | expect(isNull("true")).equals(false) 267 | expect(isNull(false)).equals(false) 268 | expect(isNull({})).equals(false) 269 | expect(isNull(undefined)).equals(false) 270 | }) 271 | }) 272 | 273 | describe("isUndefined", () => { 274 | it("should match for undefined", () => { 275 | expect(isUndefined(undefined)).equals(true) 276 | }) 277 | 278 | it("should not match for values other than undefined", () => { 279 | expect(isUndefined(NaN)).equals(false) 280 | expect(isUndefined(Infinity)).equals(false) 281 | expect(isUndefined(1)).equals(false) 282 | expect(isUndefined(0)).equals(false) 283 | expect(isUndefined(1.3)).equals(false) 284 | expect(isUndefined("true")).equals(false) 285 | expect(isUndefined(false)).equals(false) 286 | expect(isUndefined({})).equals(false) 287 | expect(isUndefined(null)).equals(false) 288 | }) 289 | }) 290 | 291 | describe("isMissing", () => { 292 | it("should match for null or undefined", () => { 293 | expect(isMissing(null)).equals(true) 294 | expect(isMissing(undefined)).equals(true) 295 | }) 296 | 297 | it("should not match for non null or undefined values", () => { 298 | expect(isMissing(NaN)).equals(false) 299 | expect(isMissing(Infinity)).equals(false) 300 | expect(isMissing(1)).equals(false) 301 | expect(isMissing(0)).equals(false) 302 | expect(isMissing(1.3)).equals(false) 303 | expect(isMissing("true")).equals(false) 304 | expect(isMissing(false)).equals(false) 305 | expect(isMissing({})).equals(false) 306 | }) 307 | }) 308 | 309 | describe("hasFields", () => { 310 | it("should match empty matcher/object", () => { 311 | expect(hasFields({})({})).equals(true) 312 | }) 313 | 314 | it("should accept extra fields", () => { 315 | expect(hasFields({})({ one: 1, two: 2 })).equals(true) 316 | }) 317 | 318 | it("should match object with matching fields", () => { 319 | expect(hasFields({ key: isNumber })({ key: 10 })).equals(true) 320 | expect(hasFields({ x: refined(isNumber)(_ => _ === 10, "IsTen") })({ x: 10 })).equals(true) 321 | expect(hasFields({ name: isString })({ x: 10, y: 20, name: "testing" })).equals(true) 322 | expect(hasFields({ 323 | a: refined(isString)(_ => _ === "one", "IsOne"), 324 | b: refined(isNumber)(_ => _ === 20, "Is20"), 325 | c: isNull 326 | })({ 327 | a: "one", 328 | b: 20, 329 | c: null 330 | })).equals(true) 331 | expect(hasFields({ length: isNumber })([])).equals(true) 332 | expect(hasFields({ length: refined(isNumber)(_ => _ === 2, "AnyVal") })([1, 2])).equals(true) 333 | }) 334 | 335 | it("should match missing fields for undefined matcher", () => { 336 | expect(hasFields({ key: isUndefined })({ value: "aloha" })).equals(true) 337 | expect(hasFields({ key: isUndefined, value: isUndefined })({})).equals(true) 338 | }) 339 | 340 | it("should not match objects with missing fields", () => { 341 | expect(hasFields({ key: isNumber })({})).equals(false) 342 | expect(hasFields({ key: isNumber, value: isString })({ value: "aloha" })).equals(false) 343 | }) 344 | 345 | it("should not match objects with wrong field types", () => { 346 | expect(hasFields({ key: isNumber })({ key: "10" })).equals(false) 347 | expect(hasFields({ key: refined(isString)(_ => _ === "20", "20") })({ key: "10" })).equals(false) 348 | expect(hasFields({ key: isNumber, value: isNumber })({ 349 | key: 10, 350 | value: "wrong number" 351 | })).equals(false) 352 | }) 353 | 354 | it("should not match other types", () => { 355 | const hf = hasFields({}) 356 | expect(hf(NaN)).equals(false) 357 | expect(hf(Infinity)).equals(false) 358 | expect(hf(1)).equals(false) 359 | expect(hf(0)).equals(false) 360 | expect(hf(1.3)).equals(false) 361 | expect(hf("true")).equals(false) 362 | expect(hf(false)).equals(false) 363 | }) 364 | }) 365 | 366 | describe("hasOptionalFields", () => { 367 | it("should match with missing fields", () => { 368 | const isNamed: TypeMatcher> = hasOptionalFields<{ name: string }>({ 369 | name: isString 370 | }) 371 | expect(isNamed({})).equals(true) 372 | }) 373 | 374 | it("should not match other types", () => { 375 | const hf = hasOptionalFields({}) 376 | expect(hf(NaN)).equals(false) 377 | expect(hf(Infinity)).equals(false) 378 | expect(hf(1)).equals(false) 379 | expect(hf(0)).equals(false) 380 | expect(hf(1.3)).equals(false) 381 | expect(hf("true")).equals(false) 382 | expect(hf(false)).equals(false) 383 | }) 384 | 385 | it("should not match objects with wrong field types", () => { 386 | expect(hasOptionalFields({ key: isNumber })({ key: "10" })).equals(false) 387 | expect(hasOptionalFields({ key: refined(isString)(_ => _ === "20", "20") })({ key: "10" })).equals(false) 388 | expect(hasOptionalFields({ key: isNumber, value: isNumber })({ 389 | key: 10, 390 | value: "wrong number" 391 | })).equals(false) 392 | }) 393 | }) 394 | 395 | describe("isObjectMapOf", () => { 396 | it("should match objects with matching properties", () => { 397 | expect(isObjectMapOf(isString)({ "one": "one", "two": "two" })).equals(true) 398 | expect(isObjectMapOf(isString)({ 1: "one", 2: "two" })).equals(true) 399 | expect(isObjectMapOf(isString)({ 1: "one", "two": "two" })).equals(true) 400 | expect(isObjectMapOf(isNumber)({ "one": 1, "two": 2 })).equals(true) 401 | expect(isObjectMapOf(isBoolean)({ "one": true, "two": false })).equals(true) 402 | expect(isObjectMapOf(isBoolean)({ 1: true, 2: false })).equals(true) 403 | 404 | class Z { 405 | readonly x: boolean 406 | 407 | constructor(x: boolean) { 408 | this.x = x 409 | } 410 | } 411 | 412 | expect(isObjectMapOf(isBoolean)(new Z(true))).equals(true) 413 | expect(isObjectMapOf(isNumber)(new Z(true))).equals(false) 414 | }) 415 | 416 | it("should not match if at least one property doesn't match", () => { 417 | expect(isObjectMapOf(isString)({ "one": "one", "two": 2 })).equals(false) 418 | expect(isObjectMapOf(isNumber)({ "one": 1, "two": false })).equals(false) 419 | expect(isObjectMapOf(isBoolean)({ "one": true, "two": "true" })).equals(false) 420 | expect(isObjectMapOf(isBoolean)({ "one": true, "two": 2 })).equals(false) 421 | expect(isObjectMapOf(isBoolean)({ "one": true, "two": {} })).equals(false) 422 | expect(isObjectMapOf(isBoolean)({ "one": true, "two": [] })).equals(false) 423 | }) 424 | }) 425 | 426 | describe("isTuple1", () => { 427 | const isT1 = isTuple1(isNumber) 428 | it("should match on valid tuples", () => { 429 | expect(isT1([10])).equals(true) 430 | expect(isTuple1(isString)(["test"])).equals(true) 431 | }) 432 | 433 | it("should not match different size tuples", () => { 434 | expect(isT1([1, 2])).equals(false) 435 | expect(isT1([])).equals(false) 436 | }) 437 | 438 | it("should not match other types", () => { 439 | expect(isT1(NaN)).equals(false) 440 | expect(isT1(Infinity)).equals(false) 441 | expect(isT1(1)).equals(false) 442 | expect(isT1(0)).equals(false) 443 | expect(isT1(1.3)).equals(false) 444 | expect(isT1("true")).equals(false) 445 | expect(isT1(false)).equals(false) 446 | expect(isT1({})).equals(false) 447 | }) 448 | }) 449 | 450 | describe("isTuple2", () => { 451 | const isT2 = isTuple2(isNumber, isString) 452 | it("should match on valid tuples", () => { 453 | expect(isT2([10, "ten"])).equals(true) 454 | }) 455 | 456 | it("should not match different size tuples", () => { 457 | expect(isT2([1, "one", 2])).equals(false) 458 | expect(isT2([1])).equals(false) 459 | }) 460 | 461 | it("should not match other types", () => { 462 | expect(isT2(NaN)).equals(false) 463 | expect(isT2(Infinity)).equals(false) 464 | expect(isT2(1)).equals(false) 465 | expect(isT2(0)).equals(false) 466 | expect(isT2(1.3)).equals(false) 467 | expect(isT2("true")).equals(false) 468 | expect(isT2(false)).equals(false) 469 | expect(isT2({})).equals(false) 470 | }) 471 | }) 472 | 473 | describe("isTuple3", () => { 474 | const isT3 = isTuple3(isNumber, isString, isBoolean) 475 | it("should match on valid tuples", () => { 476 | expect(isT3([10, "ten", false])).equals(true) 477 | }) 478 | 479 | it("should not match different size tuples", () => { 480 | expect(isT3([1, "one", true, 2])).equals(false) 481 | expect(isT3([1, "one"])).equals(false) 482 | }) 483 | 484 | it("should not match other types", () => { 485 | expect(isT3(NaN)).equals(false) 486 | expect(isT3(Infinity)).equals(false) 487 | expect(isT3(1)).equals(false) 488 | expect(isT3(0)).equals(false) 489 | expect(isT3(1.3)).equals(false) 490 | expect(isT3("true")).equals(false) 491 | expect(isT3(false)).equals(false) 492 | expect(isT3({})).equals(false) 493 | }) 494 | }) 495 | 496 | describe("isTuple4", () => { 497 | const isT4 = isTuple4(isNumber, isString, isBoolean, isNumber) 498 | it("should match on valid tuples", () => { 499 | expect(isT4([10, "ten", false, 2])).equals(true) 500 | }) 501 | 502 | it("should not match different size tuples", () => { 503 | expect(isT4([1, "one", true, 2, ""])).equals(false) 504 | expect(isT4([1, "one", true])).equals(false) 505 | }) 506 | 507 | it("should not match other types", () => { 508 | expect(isT4(NaN)).equals(false) 509 | expect(isT4(Infinity)).equals(false) 510 | expect(isT4(1)).equals(false) 511 | expect(isT4(0)).equals(false) 512 | expect(isT4(1.3)).equals(false) 513 | expect(isT4("true")).equals(false) 514 | expect(isT4(false)).equals(false) 515 | expect(isT4({})).equals(false) 516 | }) 517 | }) 518 | 519 | describe("isTuple5", () => { 520 | const isT5 = isTuple5(isNumber, isString, isBoolean, isNumber, isNumber) 521 | it("should match on valid tuples", () => { 522 | expect(isT5([10, "ten", false, 2, 3])).equals(true) 523 | }) 524 | 525 | it("should not match different size tuples", () => { 526 | expect(isT5([1, "one", true, 2, 3, ""])).equals(false) 527 | expect(isT5([1, "one", true, 2])).equals(false) 528 | }) 529 | 530 | it("should not match other types", () => { 531 | expect(isT5(NaN)).equals(false) 532 | expect(isT5(Infinity)).equals(false) 533 | expect(isT5(1)).equals(false) 534 | expect(isT5(0)).equals(false) 535 | expect(isT5(1.3)).equals(false) 536 | expect(isT5("true")).equals(false) 537 | expect(isT5(false)).equals(false) 538 | expect(isT5({})).equals(false) 539 | }) 540 | }) 541 | 542 | describe("isTuple6", () => { 543 | const isT6 = isTuple6(isNumber, isString, isBoolean, isNumber, isNumber, isString) 544 | it("should match on valid tuples", () => { 545 | expect(isT6([10, "ten", false, 2, 3, "s"])).equals(true) 546 | }) 547 | 548 | it("should not match different size tuples", () => { 549 | expect(isT6([1, "one", true, 2, 3, "s", ""])).equals(false) 550 | expect(isT6([1, "one", true, 2, 3])).equals(false) 551 | }) 552 | 553 | it("should not match other types", () => { 554 | expect(isT6(NaN)).equals(false) 555 | expect(isT6(Infinity)).equals(false) 556 | expect(isT6(1)).equals(false) 557 | expect(isT6(0)).equals(false) 558 | expect(isT6(1.3)).equals(false) 559 | expect(isT6("true")).equals(false) 560 | expect(isT6(false)).equals(false) 561 | expect(isT6({})).equals(false) 562 | }) 563 | }) 564 | 565 | describe("isTuple7", () => { 566 | const isT7 = isTuple7(isNumber, isString, isBoolean, isNumber, isNumber, isString, isNull) 567 | 568 | it("should match on valid tuples", () => { 569 | expect(isT7([10, "ten", false, 2, 3, "s", null])).equals(true) 570 | }) 571 | 572 | it("should not match different size tuples", () => { 573 | expect(isT7([1, "one", true, 2, 3, "s", null, ""])).equals(false) 574 | expect(isT7([1, "one", true, 2, 3, "s"])).equals(false) 575 | }) 576 | 577 | it("should not match other types", () => { 578 | expect(isT7(NaN)).equals(false) 579 | expect(isT7(Infinity)).equals(false) 580 | expect(isT7(1)).equals(false) 581 | expect(isT7(0)).equals(false) 582 | expect(isT7(1.3)).equals(false) 583 | expect(isT7("true")).equals(false) 584 | expect(isT7(false)).equals(false) 585 | expect(isT7({})).equals(false) 586 | }) 587 | }) 588 | 589 | describe("isTuple8", () => { 590 | const isT8 = isTuple8(isNumber, isString, isBoolean, isNumber, isNumber, isString, isNull, isBoolean) 591 | 592 | it("should match on valid tuples", () => { 593 | expect(isT8([10, "ten", false, 2, 3, "s", null, true])).equals(true) 594 | }) 595 | 596 | it("should not match different size tuples", () => { 597 | expect(isT8([1, "one", true, 2, 3, "s", null, true, ""])).equals(false) 598 | expect(isT8([1, "one", true, 2, 3, "s", null])).equals(false) 599 | }) 600 | 601 | it("should not match other types", () => { 602 | expect(isT8(NaN)).equals(false) 603 | expect(isT8(Infinity)).equals(false) 604 | expect(isT8(1)).equals(false) 605 | expect(isT8(0)).equals(false) 606 | expect(isT8(1.3)).equals(false) 607 | expect(isT8("true")).equals(false) 608 | expect(isT8(false)).equals(false) 609 | expect(isT8({})).equals(false) 610 | }) 611 | }) 612 | 613 | describe("isTuple9", () => { 614 | const isT9 = isTuple9(isNumber, isString, isBoolean, isNumber, isNumber, isString, isNull, isBoolean, isString) 615 | 616 | it("should match on valid tuples", () => { 617 | expect(isT9([10, "ten", false, 2, 3, "s", null, true, "9"])).equals(true) 618 | }) 619 | 620 | it("should not match different size tuples", () => { 621 | expect(isT9([1, "one", true, 2, 3, "s", null, true, "9", ""])).equals(false) 622 | expect(isT9([1, "one", true, 2, 3, "s", null, true])).equals(false) 623 | }) 624 | 625 | it("should not match other types", () => { 626 | expect(isT9(NaN)).equals(false) 627 | expect(isT9(Infinity)).equals(false) 628 | expect(isT9(1)).equals(false) 629 | expect(isT9(0)).equals(false) 630 | expect(isT9(1.3)).equals(false) 631 | expect(isT9("true")).equals(false) 632 | expect(isT9(false)).equals(false) 633 | expect(isT9({})).equals(false) 634 | }) 635 | }) 636 | 637 | describe("isTuple10", () => { 638 | const isT10 = isTuple10( 639 | isNumber, isString, isBoolean, isNumber, isNumber, isString, isNull, 640 | isBoolean, isString, refined(isNumber)(_ => _ === 3, "3") 641 | ) 642 | 643 | it("should match on valid tuples", () => { 644 | expect(isT10([10, "ten", false, 2, 3, "s", null, true, "9", 3])).equals(true) 645 | }) 646 | 647 | it("should not match different size tuples", () => { 648 | expect(isT10([1, "one", true, 2, 3, "s", null, true, "9", 3, ""])).equals(false) 649 | expect(isT10([1, "one", true, 2, 3, "s", null, true, "9"])).equals(false) 650 | }) 651 | 652 | it("should not match other types", () => { 653 | expect(isT10(NaN)).equals(false) 654 | expect(isT10(Infinity)).equals(false) 655 | expect(isT10(1)).equals(false) 656 | expect(isT10(0)).equals(false) 657 | expect(isT10(1.3)).equals(false) 658 | expect(isT10("true")).equals(false) 659 | expect(isT10(false)).equals(false) 660 | expect(isT10({})).equals(false) 661 | }) 662 | }) 663 | 664 | describe("isBoth", () => { 665 | it("should match when both matches", () => { 666 | expect(isBoth(isNumber, refined(isNumber)(_ => _ === 10, "10"))(10)).equals(true) 667 | expect(isBoth( 668 | hasFields({ key: isString }), 669 | hasFields({ value: refined(isNumber)(_ => _ === 10, "10") }) 670 | )({ 671 | key: "key", 672 | value: 10 673 | })).equals(true) 674 | }) 675 | 676 | it("should not match when one does't match", () => { 677 | expect(isBoth(isNumber, refined(isNumber)(_ => _ === 20, "20"))(30)).equals(false) 678 | expect(isBoth(isString, isNumber)(10)).equals(false) 679 | }) 680 | }) 681 | 682 | describe("isEither", () => { 683 | it("should match when one matches", () => { 684 | expect(isEither(isNumber, refined(isNumber)(_ => _ === 20, "20"))(10)).equals(true) 685 | expect(isEither(isNumber, isString)(10)).equals(true) 686 | expect(isEither(isNumber, isString)("str")).equals(true) 687 | }) 688 | 689 | it("should not match when none match", () => { 690 | expect(isBoth(isNumber, refined(isNumber)(_ => _ === 20, "20"))("str")).equals(false) 691 | expect(isBoth(isString, refined(isNumber)(_ => _ === 30, "30"))(10)).equals(false) 692 | }) 693 | }) 694 | 695 | describe("isOptional", () => { 696 | it("should match for valid value or undefined", () => { 697 | expect(isOptional(isNumber)(10)).equals(true) 698 | expect(isOptional(isString)(undefined)).equals(true) 699 | expect(isOptional(isNever)(undefined)).equals(true) 700 | }) 701 | 702 | it("should not match for other types", () => { 703 | expect(isOptional(refined(isNumber)(_ => _ === -1, "-1"))(NaN)).equals(false) 704 | expect(isOptional(refined(isNumber)(_ => _ === -1, "-1"))(Infinity)).equals(false) 705 | expect(isOptional(refined(isNumber)(_ => _ === -1, "-1"))(1)).equals(false) 706 | expect(isOptional(refined(isNumber)(_ => _ === -1, "-1"))(0)).equals(false) 707 | expect(isOptional(refined(isNumber)(_ => _ === -1, "-1"))(1.3)).equals(false) 708 | expect(isOptional(refined(isNumber)(_ => _ === -1, "-1"))("true")).equals(false) 709 | expect(isOptional(refined(isNumber)(_ => _ === -1, "-1"))(false)).equals(false) 710 | expect(isOptional(refined(isNumber)(_ => _ === -1, "-1"))({})).equals(false) 711 | expect(isOptional(refined(isNumber)(_ => _ === -1, "-1"))(null)).equals(false) 712 | }) 713 | }) 714 | 715 | describe("isNullable", () => { 716 | it("should match for valid value or null", () => { 717 | expect(isNullable(isNumber)(10)).equals(true) 718 | expect(isNullable(isString)(null)).equals(true) 719 | expect(isNullable(isNever)(null)).equals(true) 720 | }) 721 | 722 | it("should not match for other types", () => { 723 | expect(isNullable(refined(isNumber)(_ => _ === -1, "-1"))(NaN)).equals(false) 724 | expect(isNullable(refined(isNumber)(_ => _ === -1, "-1"))(Infinity)).equals(false) 725 | expect(isNullable(refined(isNumber)(_ => _ === -1, "-1"))(1)).equals(false) 726 | expect(isNullable(refined(isNumber)(_ => _ === -1, "-1"))(0)).equals(false) 727 | expect(isNullable(refined(isNumber)(_ => _ === -1, "-1"))(1.3)).equals(false) 728 | expect(isNullable(refined(isNumber)(_ => _ === -1, "-1"))("true")).equals(false) 729 | expect(isNullable(refined(isNumber)(_ => _ === -1, "-1"))(false)).equals(false) 730 | expect(isNullable(refined(isNumber)(_ => _ === -1, "-1"))({})).equals(false) 731 | expect(isNullable(refined(isNumber)(_ => _ === -1, "-1"))(undefined)).equals(false) 732 | }) 733 | }) 734 | 735 | describe("refined", () => { 736 | it("should return a TypeMatcher for a refined type", () => { 737 | const match: TypeMatcher> = refined(isNumber)(_ => _ > 0, "Positive") 738 | expect(match(1)).equals(true, "1 is positive") 739 | // You may be wondering is 0 positive or not?! Answer: it depends ... it doesn't matter for this test 740 | expect(match(0)).equals(false, "0 is not positive (?)") 741 | expect(match(-1)).equals(false, "-1 is not positive") 742 | }) 743 | 744 | it("works with isBoth", () => { 745 | const isGreaterThan10: TypeMatcher> = refined(isNumber)(_ => _ > 10, "GreaterThan10") 746 | const isLowerThan20: TypeMatcher> = refined(isNumber)(_ => _ < 20, "LowerThan20") 747 | 748 | // some type tests, by compiler 749 | const inRange: TypeMatcher> = isBoth(isGreaterThan10, isLowerThan20) 750 | const a: TypeMatcher = isGreaterThan10 751 | const b: TypeMatcher> = inRange 752 | const c: TypeMatcher> = inRange 753 | 754 | expect(isGreaterThan10(11)).equals(true) 755 | expect(isGreaterThan10(10)).equals(false) 756 | expect(isLowerThan20(19)).equals(true) 757 | expect(isLowerThan20(20)).equals(false) 758 | 759 | expect(inRange(10)).equals(false) 760 | expect(inRange(11)).equals(true) 761 | expect(inRange(19)).equals(true) 762 | expect(inRange(20)).equals(false) 763 | }) 764 | 765 | it("works with isEither", () => { 766 | const isGreaterThan20: TypeMatcher> = refined(isNumber)(_ => _ > 20, "GreaterThan20") 767 | const isLowerThan10: TypeMatcher> = refined(isNumber)(_ => _ < 10, "LowerThan10") 768 | 769 | // some type tests, by compiler 770 | const inRange: TypeMatcher | Refined> = isEither(isGreaterThan20, isLowerThan10) 771 | const inRange2: TypeMatcher> = isEither(isGreaterThan20, isLowerThan10) 772 | 773 | const a: TypeMatcher = isGreaterThan20 774 | 775 | expect(isGreaterThan20(21)).equals(true) 776 | expect(isGreaterThan20(20)).equals(false) 777 | expect(isLowerThan10(9)).equals(true) 778 | expect(isLowerThan10(10)).equals(false) 779 | 780 | expect(inRange(10)).equals(false) 781 | expect(inRange(20)).equals(false) 782 | expect(inRange(9)).equals(true) 783 | expect(inRange(21)).equals(true) 784 | }) 785 | }) 786 | 787 | describe("failWith", () => { 788 | it("will return true on match", () => { 789 | expect(failWith(new Error("Invalid value, string expected"))(isString)("aloha")) 790 | .equals(true, "new matcher returns true") 791 | }) 792 | 793 | it("will throw on matcher miss", () => { 794 | expect(() => failWith(new Error("Invalid value, string expected"))(isString)(10)) 795 | .to.throw("Invalid value, string expected") 796 | }) 797 | }) 798 | }) 799 | 800 | describe("Match DSL", function () { 801 | describe("match", () => { 802 | it("calls Case.map function over input value and returns result", () => { 803 | expect(match(10, 804 | caseWhen(isAny, ten => { 805 | expect(ten).equals(10) 806 | return "executed" 807 | }) 808 | )).equals("executed") 809 | }) 810 | }) 811 | 812 | describe("matcher", () => { 813 | it("returns a function which calls Case.map", () => { 814 | const numberToString: (n: number) => string = matcher(caseWhen(isNumber, n => `${n}`)) 815 | expect(numberToString(10)).equals("10") 816 | }) 817 | }) 818 | 819 | describe("caseWhen", () => { 820 | it("builds new match case", () => { 821 | const isOne: TypeMatcher<"one"> = (val: any): val is "one" => val === "one" 822 | const c: MatchCase<"one", 1> = caseWhen(isOne, (one): 1 => 1) 823 | expect(c.map("one")).equals(1) 824 | }) 825 | 826 | it("will throw on error when input doesn't match (may be caused by buggy type matchers", () => { 827 | const isTen: TypeMatcher<10> = (val: any): val is 10 => false 828 | expect(() => caseWhen(isTen, _ => _ * 2).map(10)).throws("No match") 829 | }) 830 | 831 | it("should exhaustive check input value type", () => { 832 | // Unfortunately there is no way (know to me) to check compilation failures :( 833 | const x: number = match("1" as string | number, 834 | caseWhen(isString, _ => 10). 835 | caseWhen(isNumber, _ => _) 836 | ) 837 | }) 838 | 839 | it("should compose result types", () => { 840 | const cases = caseWhen(isBoolean, (_): 0 => 0) 841 | .caseWhen(isString, (_): 1 => 1) 842 | .caseWhen(isNumber, (_): 2 => 2) 843 | 844 | const x1: 0 | 1 | 2 = cases.map(true) 845 | expect(x1).equals(0) 846 | 847 | const x2: 0 | 1 | 2 = cases.map("hello") 848 | expect(x2).equals(1) 849 | 850 | const x3: 0 | 1 | 2 = cases.map(10) 851 | expect(x3).equals(2) 852 | 853 | const y1: 0 | 1 | 2 | 3 = caseWhen(isBoolean, _ => _ ? 1 : 0) // result type inferred as 1 | 0 854 | .caseWhen(isString, _ => _.length > 0 ? 2 : 1) // result type inferred as 2 | 1 855 | .caseWhen(isNumber, (_): 3 => 3) 856 | .map("hello") 857 | }) 858 | 859 | it("should check cases in order those was defined", () => { 860 | let cnt = 0 861 | const cases = caseWhen( 862 | (val: any): val is boolean => { 863 | expect(cnt).equals(0) 864 | cnt += 1 865 | return isBoolean(val) 866 | }, 867 | () => cnt 868 | ). 869 | caseWhen( 870 | (val: any): val is string => { 871 | expect(cnt).equals(1) 872 | cnt += 1 873 | return isString(val) 874 | }, 875 | () => cnt 876 | ). 877 | caseWhen( 878 | (val: any): val is number => { 879 | expect(cnt).equals(2) 880 | cnt += 1 881 | return isNumber(val) 882 | }, 883 | () => cnt 884 | ). 885 | caseWhen( 886 | (val: any): val is number => { 887 | expect(cnt).equals(3) 888 | cnt += 1 889 | return isFiniteNumber(val) 890 | }, 891 | () => cnt 892 | ). 893 | caseWhen( 894 | (val: any): val is object => { 895 | expect(cnt).equals(4) 896 | cnt += 1 897 | return isObject(val) 898 | }, 899 | () => cnt 900 | ) 901 | 902 | expect(cases.map({})).equals(5) 903 | }) 904 | }) 905 | 906 | describe("caseDefault", () => { 907 | it("returns function result for any given input", () => { 908 | expect(caseDefault(() => 10).map("hello1")).equals(10) 909 | expect(caseDefault(() => "hola").map("hello2")).equals("hola") 910 | expect(caseDefault(() => true).map("hello3")).equals(true) 911 | expect(caseDefault(() => false).map("hello4")).equals(false) 912 | }) 913 | }) 914 | }) 915 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "strict": true, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitUseStrict": false, 12 | "sourceMap": true, 13 | "allowJs": false, 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmitOnError": true, 16 | "declaration": true 17 | }, 18 | "include": [ 19 | "src/**/*.ts" 20 | ], 21 | "exclude": [ 22 | "node_modules", 23 | "dist" 24 | ] 25 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint-config-standard", 4 | "tslint-eslint-rules" 5 | ], 6 | "rules": { 7 | "file-header": [true, "Copyright"], 8 | "space-before-function-paren": false, 9 | "no-use-before-declare": false, 10 | "no-empty": false, 11 | "no-unused-expression": false, 12 | "no-duplicate-variable": true, 13 | "no-var-keyword": true, 14 | "member-ordering": ["error", "fields-first"], 15 | "quotemark": [true, "double", "avoid-escape"], 16 | "curly": false, 17 | "ter-indent": false, 18 | "no-unused-variable": false, 19 | "brace-style": false, 20 | "one-line": false 21 | } 22 | } --------------------------------------------------------------------------------