├── .eslintrc.json ├── .github └── workflows │ └── ci-test.yaml ├── .gitignore ├── HISTORY.md ├── LICENSE ├── README.md ├── babel-cjs.config.json ├── babel.config.json ├── benchmark └── benchmark.mjs ├── examples ├── any_type.mjs ├── basic_usage.mjs ├── browser.html ├── custom_type.mjs ├── merge_plain_functions.mjs ├── merge_typed_functions.mjs ├── multiple_signatures.mjs ├── recursion.mjs ├── rest_parameters.mjs └── type_conversion.mjs ├── package-lock.json ├── package.json ├── src └── typed-function.mjs ├── test-lib ├── apps │ ├── cjsApp.cjs │ └── esmApp.mjs └── lib.test.cjs ├── test ├── any_type.test.mjs ├── browserEsmBuild.html ├── browserSrc.html ├── compose.test.mjs ├── construction.test.mjs ├── conversion.test.js ├── convert.test.mjs ├── errors.test.mjs ├── find.test.mjs ├── isTypedFunction.test.mjs ├── merge.test.mjs ├── onMismatch.test.mjs ├── resolve.test.mjs ├── rest_params.mjs ├── security.test.mjs ├── strictEqualArray.mjs └── union_types.test.mjs └── tools └── cjs └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "mocha": true 6 | }, 7 | "extends": [ 8 | "standard" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": "latest", 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/ci-test.yaml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ develop, master ] 9 | pull_request: 10 | branches: [ develop, master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x, 20.x, 22.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build-and-test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Coverage directory used by tools like istanbul 6 | coverage 7 | 8 | # Build outputs 9 | lib 10 | 11 | # Dependency directory 12 | # Commenting this out is preferred by some people, see 13 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 14 | node_modules 15 | 16 | # Users Environment Variables 17 | .lock-wscript 18 | 19 | # WebStorm settings 20 | .idea 21 | 22 | # Cloud9 settings 23 | .c9 24 | 25 | # eslint 26 | .eslintcache 27 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | 4 | ## 2024-06-05, version 4.2.1 5 | 6 | - Fix a bug in the new `override` option of method `addConversion`. 7 | 8 | 9 | ## 2024-06-05, version 4.2.0 10 | 11 | - Extend methods `addConversion` and `addConversions` with a new option 12 | `{ override: boolean }` to allow overriding an existing conversion. 13 | 14 | 15 | ## 2023-09-13, version 4.1.1 16 | 17 | - Fix #168: add a `"license": "MIT"` field to the `package.json` file. 18 | 19 | 20 | ## 2022-08-23, version 4.1.0 21 | 22 | - Publish an UMD version of the library, like in v3.0.0. It is still necessary. 23 | The UMD version can be used in CommonJS applications and in the browser. 24 | 25 | 26 | ## 2022-08-22, version 4.0.0 27 | 28 | !!! BE CAREFUL: BREAKING CHANGES !!! 29 | 30 | - Breaking change: the code is converted into ES modules, and the library 31 | now outputs ES modules only instead of an UMD module. 32 | - If you're using `typed-function` inside and ES modules project, 33 | all will just keep working like before: 34 | ```js 35 | import typed from 'typed-function' 36 | ``` 37 | - If you're using `typed-function` in a CommonJS project, you'll have to 38 | import the library using a dynamic import: 39 | ```js 40 | const typed = (await import('typed-function')).default 41 | ``` 42 | - If you're importing `typed-function` straight into a browser page, 43 | you can load it as a module there: 44 | ```html 45 | 48 | ``` 49 | 50 | 51 | ## 2022-08-16, version 3.0.1 52 | 53 | - Fix #157: `typed()` can enter infinite loop when there is both `referToSelf` 54 | and `referTo` functions involved (#158). Thanks @gwhitney. 55 | - Fix #155: `typed.addType()` fails if there is no `Object` type (#159). 56 | Thanks @gwhitney. 57 | 58 | 59 | ## 2022-05-12, version 3.0.0 60 | 61 | !!! BE CAREFUL: BREAKING CHANGES !!! 62 | 63 | Breaking changes: 64 | 65 | - Fix #14: conversions now have preference over `any`. Thanks @gwhitney. 66 | 67 | - The properties `typed.types` and `typed.conversions` have been removed. 68 | Instead of adding and removing types and conversions with those 69 | arrays, use the methods `addType`, `addTypes`, `addConversion`, 70 | `addConversions`, `removeConversion`, `clear`, `clearConversions`. 71 | 72 | - The `this` variable is no longer bound to the typed function itself but is 73 | unbound. Instead, use `typed.referTo(...)` and `typed.referToSelf(...)`. 74 | 75 | By default, all function bodies will be scanned against the deprecated 76 | usage pattern of `this`, and an error will be thrown when encountered. To 77 | disable this validation step, set `typed.warnAgainstDeprecatedThis = false`. 78 | 79 | Example: 80 | 81 | ```js 82 | // old: 83 | const square = typed({ 84 | 'number': x => x * x, 85 | 'string': x => this(parseFloat(x)) 86 | }) 87 | 88 | // new: 89 | const square = typed({ 90 | 'number': x => x * x, 91 | 'string': typed.referToSelf(function (self) { 92 | // using self is not optimal, if possible, 93 | // refer to a specific signature instead, 94 | // see next example 95 | return x => self(parseFloat(x)) 96 | }) 97 | }) 98 | 99 | // optimized new: 100 | const square = typed({ 101 | 'number': x => x * x, 102 | 'string': typed.referTo('number', function (squareNumber) { 103 | return x => sqrtNumber(parseFloat(x)) 104 | }) 105 | }) 106 | ``` 107 | 108 | - The property `typed.ignore` is removed. If you need it, see if you can 109 | create a new `typed` instance without the types that you want to ignore, or 110 | filter the signatures passed to `typed()` by hand. 111 | - Drop official support for Nodejs 12. 112 | 113 | Non-breaking changes: 114 | 115 | - Implemented new static functions, Thanks @gwhitney: 116 | - `typed.referTo(...string, callback: (resolvedFunctions: ...function) => function)` 117 | - `typed.referToSelf(callback: (self) => function)` 118 | - `typed.isTypedFunction(entity: any): boolean` 119 | - `typed.resolve(fn: typed-function, argList: Array): signature-object` 120 | - `typed.findSignature(fn: typed-function, signature: string | Array, options: object) : signature-object` 121 | - `typed.addType(type: {name: string, test: function, ignored?: boolean} [, beforeObjectTest=true]): void` 122 | - `typed.addTypes(types: TypeDef[] [, before = 'any']): void` 123 | - `typed.clear(): void` 124 | - `typed.addConversions(conversions: ConversionDef[]): void` 125 | - `typed.removeConversion(conversion: ConversionDef): void` 126 | - `typed.clearConversions(): void` 127 | - Refactored the `typed` constructor to be more flexible, accepting a 128 | combination of multiple typed functions or objects. And internally refactored 129 | the constructor to not use typed-function itself (#142). Thanks @gwhitney. 130 | - Extended the benchmark script and added counting of creation of typed 131 | functions (#146). 132 | - Fixes and extensions to `typed.find()` now correctly handling cases with 133 | rest or `any` parameters and matches requiring conversions; adds an 134 | `options` argument to control whether matches with conversions are allowed. 135 | Thanks @gwhitney. 136 | - Fix to `typed.convert()`: Will now find a conversion even in presence of 137 | overlapping types. 138 | - Reports all matching types in runtime errors, not just the first one. 139 | - Improved documentation. Thanks @gwhitney. 140 | 141 | 142 | ## 2022-03-11, version 2.1.0 143 | 144 | - Implemented configurable callbacks `typed.createError` and `typed.onMismatch`. 145 | Thanks @gwhitney. 146 | 147 | 148 | ## 2020-07-03, version 2.0.0 149 | 150 | - Drop official support for node.js 6 and 8, though no breaking changes 151 | at this point. 152 | - Implemented support for recursion using the `this` keyword. Thanks @nickewing. 153 | 154 | 155 | ## 2019-08-22, version 1.1.1 156 | 157 | - Fix #15: passing `null` to an `Object` parameter throws wrong error. 158 | 159 | 160 | ## 2018-07-28, version 1.1.0 161 | 162 | - Implemented support for creating typed functions from a plain function 163 | having a property `signature`. 164 | - Implemented providing a name when merging multiple typed functions. 165 | 166 | 167 | ## 2018-07-04, version 1.0.4 168 | 169 | - By default, `addType` will insert new types before the `Object` test 170 | since the `Object` test also matches arrays and classes. 171 | - Upgraded `devDependencies`. 172 | 173 | 174 | ## 2018-03-17, version 1.0.3 175 | 176 | - Dropped usage of ES6 feature `Array.find`, so typed-function is 177 | directly usable on any ES5 compatible JavaScript engine (like IE11). 178 | 179 | 180 | ## 2018-03-17, version 1.0.2 181 | 182 | - Fixed typed-function not working on browsers that don't allow 183 | setting the `name` property of a function. 184 | 185 | 186 | ## 2018-02-21, version 1.0.1 187 | 188 | - Upgraded dev dependencies. 189 | 190 | 191 | ## 2018-02-20, version 1.0.0 192 | 193 | Version 1.0.0 is rewritten from scratch. The API is the same, 194 | though generated error messages may differ slightly. 195 | 196 | Version 1.0.0 no longer uses `eval` under the hood to achieve good 197 | performance. This reduces security risks and makes typed-functions 198 | easier to debug. 199 | 200 | Type `Object` is no longer treated specially from other types. This 201 | means that the test for `Object` must not give false positives for 202 | types like `Array`, `Date`, or class instances. 203 | 204 | In version 1.0.0, support for browsers like IE9, IE10 is dropped, 205 | though typed-function can still work when using es5 and es6 polyfills. 206 | 207 | 208 | ## 2018-01-24, version 0.10.7 209 | 210 | - Fixed the field `data.actual` in a `TypeError` message containing 211 | the type index instead of the actual type of the argument. 212 | 213 | 214 | ## 2017-11-18, version 0.10.6 215 | 216 | - Fixed a security issue allowing to execute arbitrary JavaScript 217 | code via a specially prepared function name of a typed function. 218 | Thanks Masato Kinugawa. 219 | 220 | 221 | ## 2016-11-18, version 0.10.5 222 | 223 | - Fixed the use of multi-layered use of `any` type. See #8. 224 | 225 | 226 | ## 2016-04-09, version 0.10.4 227 | 228 | - Typed functions can only inherit names from other typed functions and no 229 | longer from regular JavaScript functions since these names are unreliable: 230 | they can be manipulated by minifiers and browsers. 231 | 232 | 233 | ## 2015-10-07, version 0.10.3 234 | 235 | - Reverted the fix of v0.10.2 until the introduced issue with variable 236 | arguments is fixed too. Added unit test for the latter case. 237 | 238 | 239 | ## 2015-10-04, version 0.10.2 240 | 241 | - Fixed support for using `any` multiple times in a single signture. 242 | Thanks @luke-gumbley. 243 | 244 | 245 | ## 2015-07-27, version 0.10.1 246 | 247 | - Fixed functions `addType` and `addConversion` not being robust against 248 | replaced arrays `typed.types` and `typed.conversions`. 249 | 250 | 251 | ## 2015-07-26, version 0.10.0 252 | 253 | - Dropped support for the following construction signatures in order to simplify 254 | the API: 255 | - `typed(signature: string, fn: function)` 256 | - `typed(name: string, signature: string, fn: function)` 257 | - Implemented convenience methods `typed.addType` and `typed.addConversion`. 258 | - Changed the casing of the type `'function'` to `'Function'`. Breaking change. 259 | - `typed.types` is now an ordered Array containing objects 260 | `{name: string, test: function}`. Breaking change. 261 | - List with expected types in error messages no longer includes converted types. 262 | 263 | 264 | ## 2015-05-17, version 0.9.0 265 | 266 | - `typed.types` is now an ordered Array containing objects 267 | `{type: string, test: function}` instead of an object. Breaking change. 268 | - `typed-function` now allows merging typed functions with duplicate signatures 269 | when they point to the same function. 270 | 271 | 272 | ## 2015-05-16, version 0.8.3 273 | 274 | - Function `typed.find` now throws an error instead of returning `null` when a 275 | signature is not found. 276 | - Fixed: the attached signatures no longer contains signatures with conversions. 277 | 278 | 279 | ## 2015-05-09, version 0.8.2 280 | 281 | - Fixed function `typed.convert` not handling the case where the value already 282 | has the requested type. Thanks @rjbaucells. 283 | 284 | 285 | ## 2015-05-09, version 0.8.1 286 | 287 | - Implemented option `typed.ignore` to ignore/filter signatures of a typed 288 | function. 289 | 290 | 291 | ## 2015-05-09, version 0.8.0 292 | 293 | - Implemented function `create` to create a new instance of typed-function. 294 | - Implemented a utility function `convert(value, type)` (#1). 295 | - Implemented a simple `typed.find` function to find the implementation of a 296 | specific function signature. 297 | - Extended the error messages to denote the function name, like `"Too many 298 | arguments in function foo (...)"`. 299 | 300 | 301 | ## 2015-04-17, version 0.7.0 302 | 303 | - Performance improvements. 304 | 305 | 306 | ## 2015-03-08, version 0.6.3 307 | 308 | - Fixed generated internal Signature and Param objects not being cleaned up 309 | after the typed function has been generated. 310 | 311 | 312 | ## 2015-02-26, version 0.6.2 313 | 314 | - Fixed a bug sometimes not ordering the handling of any type arguments last. 315 | - Fixed a bug sometimes not choosing the signature with the lowest number of 316 | conversions. 317 | 318 | 319 | ## 2015-02-07, version 0.6.1 320 | 321 | - Large code refactoring. 322 | - Fixed bugs related to any type parameters. 323 | 324 | 325 | ## 2015-01-16, version 0.6.0 326 | 327 | - Removed the configuration option `minify` 328 | (it's not clear yet whether minifying really improves the performance). 329 | - Internal code simplifications. 330 | - Bug fixes. 331 | 332 | 333 | ## 2015-01-07, version 0.5.0 334 | 335 | - Implemented support for merging typed functions. 336 | - Typed functions inherit the name of the function in case of one signature. 337 | - Fixed a bug where a regular argument was not matched when there was a 338 | signature with variable arguments too. 339 | - Slightly changed the error messages. 340 | 341 | 342 | ## 2014-12-17, version 0.4.0 343 | 344 | - Introduced new constructor options, create a typed function as 345 | `typed([name,] signature, fn)` or `typed([name,] signatures)`. 346 | - Support for multiple types per parameter like `number | string, number'`. 347 | - Support for variable parameters like `sting, ...number'`. 348 | - Changed any type notation `'*'` to `'any'`. 349 | - Implemented detailed error messages. 350 | - Implemented option `typed.config.minify`. 351 | 352 | 353 | ## 2014-11-05, version 0.3.1 354 | 355 | - Renamed module to `typed-function`. 356 | 357 | 358 | ## 2014-11-05, version 0.3.0 359 | 360 | - Implemented support for any type arguments (denoted with `*`). 361 | 362 | 363 | ## 2014-10-23, version 0.2.0 364 | 365 | - Implemented support for named functions. 366 | - Implemented support for type conversions. 367 | - Implemented support for custom types. 368 | - Library packaged as UMD, usable with CommonJS (node.js), AMD, and browser globals. 369 | 370 | 371 | ## 2014-10-21, version 0.1.0 372 | 373 | - Implemented support for functions with zero, one, or multiple arguments. 374 | 375 | 376 | ## 2014-10-19, version 0.0.1 377 | 378 | - First release (no functionality yet) 379 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2024 Jos de Jong 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 | # typed-function 2 | 3 | [![Version](https://img.shields.io/npm/v/typed-function.svg)](https://www.npmjs.com/package/typed-function) 4 | [![Downloads](https://img.shields.io/npm/dm/typed-function.svg)](https://www.npmjs.com/package/typed-function) 5 | [![Build Status](https://github.com/josdejong/typed-function/workflows/Node.js%20CI/badge.svg)](https://github.com/josdejong/typed-function/actions) 6 | 7 | Move type checking logic and type conversions outside of your function in a 8 | flexible, organized way. Automatically throw informative errors in case of 9 | wrong input arguments. 10 | 11 | 12 | ## Features 13 | 14 | typed-function has the following features: 15 | 16 | - Runtime type-checking of input arguments. 17 | - Automatic type conversion of arguments. 18 | - Compose typed functions with multiple signatures. 19 | - Supports union types, any type, and variable arguments. 20 | - Detailed error messaging. 21 | 22 | Supported environments: node.js, Chrome, Firefox, Safari, Opera, IE11+. 23 | 24 | 25 | ## Why? 26 | 27 | In JavaScript, functions can be called with any number and any type of arguments. 28 | When writing a function, the easiest way is to just assume that the function 29 | will be called with the correct input. This leaves the function's behavior on 30 | invalid input undefined. The function may throw some error, or worse, 31 | it may silently fail or return wrong results. Typical errors are 32 | *TypeError: undefined is not a function* or *TypeError: Cannot call method 33 | 'request' of undefined*. These error messages are not very helpful. It can be 34 | hard to debug them, as they can be the result of a series of nested function 35 | calls manipulating and propagating invalid or incomplete data. 36 | 37 | Often, JavaScript developers add some basic type checking where it is important, 38 | using checks like `typeof fn === 'function'`, `date instanceof Date`, and 39 | `Array.isArray(arr)`. For functions supporting multiple signatures, 40 | the type checking logic can grow quite a bit, and distract from the actual 41 | logic of the function. 42 | 43 | For functions dealing with a considerable amount of type checking and conversion 44 | logic, or functions facing a public API, it can be very useful to use the 45 | `typed-function` module to handle the type-checking logic. This way: 46 | 47 | - Users of the function get useful and consistent error messages when using 48 | the function wrongly. 49 | - The function cannot silently fail or silently give wrong results due to 50 | invalid input. 51 | - Correct type of input is assured inside the function. The function's code 52 | becomes easier to understand as it only contains the actual function logic. 53 | Lower level utility functions called by the type-checked function can 54 | possibly be kept simpler as they don't need to do additional type checking. 55 | 56 | It's important however not to *overuse* type checking: 57 | 58 | - Locking down the type of input that a function accepts can unnecessarily 59 | limit its flexibility. Keep functions as flexible and forgiving as possible, 60 | follow the 61 | [robustness principle](http://en.wikipedia.org/wiki/Robustness_principle) 62 | here: "be liberal in what you accept and conservative in what you send" 63 | (Postel's law). 64 | - There is no need to apply type checking to *all* functions. It may be 65 | enough to apply type checking to one tier of public facing functions. 66 | - There is a performance penalty involved for all type checking, so applying 67 | it everywhere can unnecessarily worsen the performance. 68 | 69 | 70 | ## Load 71 | 72 | Install via npm: 73 | 74 | npm install typed-function 75 | 76 | 77 | ## Usage 78 | 79 | Here are some usage examples. More examples are available in the 80 | [/examples](/examples) folder. 81 | 82 | ```js 83 | import typed from 'typed-function' 84 | 85 | // create a typed function 86 | var fn1 = typed({ 87 | 'number, string': function (a, b) { 88 | return 'a is a number, b is a string'; 89 | } 90 | }); 91 | 92 | // create a typed function with multiple types per argument (type union) 93 | var fn2 = typed({ 94 | 'string, number | boolean': function (a, b) { 95 | return 'a is a string, b is a number or a boolean'; 96 | } 97 | }); 98 | 99 | // create a typed function with any type argument 100 | var fn3 = typed({ 101 | 'string, any': function (a, b) { 102 | return 'a is a string, b can be anything'; 103 | } 104 | }); 105 | 106 | // create a typed function with multiple signatures 107 | var fn4 = typed({ 108 | 'number': function (a) { 109 | return 'a is a number'; 110 | }, 111 | 'number, boolean': function (a, b) { 112 | return 'a is a number, b is a boolean'; 113 | }, 114 | 'number, number': function (a, b) { 115 | return 'a is a number, b is a number'; 116 | } 117 | }); 118 | 119 | // create a typed function from a plain function with signature 120 | function fnPlain (a, b) { 121 | return 'a is a number, b is a string'; 122 | } 123 | 124 | fnPlain.signature = 'number, string'; 125 | var fn5 = typed(fnPlain); 126 | 127 | // use the functions 128 | console.log(fn1(2, 'foo')); // outputs 'a is a number, b is a string' 129 | console.log(fn4(2)); // outputs 'a is a number' 130 | 131 | // calling the function with a non-supported type signature will throw an error 132 | try { 133 | fn2('hello', 'world'); 134 | } catch (err) { 135 | console.log(err.toString()); 136 | // outputs: TypeError: Unexpected type of argument. 137 | // Expected: number or boolean, actual: string, index: 1. 138 | } 139 | ``` 140 | 141 | 142 | ## Types 143 | 144 | typed-function has the following built-in types: 145 | 146 | - `null` 147 | - `boolean` 148 | - `number` 149 | - `string` 150 | - `Function` 151 | - `Array` 152 | - `Date` 153 | - `RegExp` 154 | - `Object` 155 | 156 | The following type expressions are supported: 157 | 158 | - Multiple arguments: `string, number, Function` 159 | - Union types: `number | string` 160 | - Variable arguments: `...number` 161 | - Any type: `any` 162 | 163 | ### Dispatch 164 | 165 | When a typed function is called, an implementation with a matching signature 166 | is called, where conversions may be applied to actual arguments in order to 167 | find a match. 168 | 169 | Among all matching signatures, the one to execute is chosen by the following 170 | preferences, in order of priority: 171 | 172 | * one that does not have an `...any` parameter 173 | * one with the fewest `any` parameters 174 | * one that does not use conversions to match a rest parameter 175 | * one with the fewest conversions needed to match overall 176 | * one with no rest parameter 177 | * If there's a rest parameter, the one with the most non-rest parameters 178 | * The one with the largest number of preferred parameters 179 | * The one with the earliest preferred parameter 180 | 181 | When this process gets to the point of comparing individual parameters, 182 | the preference between parameters is determined by the following, in 183 | priority order: 184 | 185 | * All specific types are preferred to the 'any' type 186 | * All directly matching types are preferred to conversions 187 | * Types earlier in the list of known types are preferred 188 | * Among conversions, ones earlier in the list are preferred 189 | 190 | If none of these aspects produces a preference, then in those contexts in 191 | which Array.sort is stable, the order implementations were listed when 192 | the typed-function was created breaks the tie. Otherwise the dispatch may 193 | select any of the "tied" implementations. 194 | 195 | ## API 196 | 197 | ### Construction 198 | 199 | ``` 200 | typed([name: string], ...Object.|function) 201 | ``` 202 | A typed function can be constructed from an optional name and any number of 203 | (additional) arguments that supply the implementations for various 204 | signatures. Each of these further arguments must be one of the following: 205 | 206 | - An object with one or multiple signatures, i.e. a plain object 207 | with string keys, each of which names a signature, and functions as 208 | the values of those keys. 209 | 210 | - A previously constructed typed function, in which case all of its 211 | signatures and corresponding implementations are merged into the new 212 | typed function. 213 | 214 | - A plain function with a `signature` property whose value is a string 215 | giving that function's signature. 216 | 217 | The name, if specified, must be the first argument. If not specified, the new 218 | typed-function's name is inherited from the arguments it is composed from, 219 | as long as any that have names agree with one another. 220 | 221 | If the same signature is specified by the collection of arguments more than 222 | once with different implementations, an error will be thrown. 223 | 224 | #### Properties and methods of a typed function `fn` 225 | 226 | - `fn.name : string` 227 | 228 | The name of the typed function, if one was assigned at creation; otherwise, 229 | the value of this property is the empty string. 230 | 231 | - `fn.signatures : Object.` 232 | 233 | The value of this property is a plain object. Its keys are the string 234 | signatures on which this typed function `fn` is directly defined 235 | (without conversions). The value for each key is the function `fn` 236 | will call when its arguments match that signature. This property may 237 | differ from the similar object used to create the typed function, 238 | in that the originally provided signatures are parsed into a canonical, 239 | more usable form: union types are split into their constituents where 240 | possible, whitespace in the signature strings is removed, etc. 241 | 242 | - `fn.toString() : string` 243 | 244 | Returns human-readable code showing exactly what the function does. 245 | Mostly for debugging purposes. 246 | 247 | ### Methods of the typed package 248 | 249 | - `typed.convert(value: *, type: string) : *` 250 | 251 | Convert a value to another type. Only applicable when conversions have 252 | been added with `typed.addConversion()` and/or `typed.addConversions()` 253 | (see below in the method list). 254 | Example: 255 | 256 | ```js 257 | typed.addConversion({ 258 | from: 'number', 259 | to: 'string', 260 | convert: function (x) { 261 | return +x; 262 | } 263 | }); 264 | 265 | var str = typed.convert(2.3, 'string'); // '2.3' 266 | ``` 267 | 268 | - `typed.create() : function` 269 | 270 | Create a new, isolated instance of typed-function. Example: 271 | 272 | ```js 273 | import typed from 'typed-function.mjs'; // default instance 274 | const typed2 = typed.create(); // a second instance 275 | ``` 276 | 277 | This would allow you, for example, to have two different type hierarchies 278 | for different purposes. 279 | 280 | - `typed.resolve(fn: typed-function, argList: Array): signature-object` 281 | 282 | Find the specific signature and implementation that the typed function 283 | `fn` will call if invoked on the argument list `argList`. Returns null if 284 | there is no matching signature. The returned signature object has 285 | properties `params`, `test`, `fn`, and `implementation`. The difference 286 | between the last two properties is that `fn` is the original function 287 | supplied at typed-function creation time, whereas `implementation` is 288 | ready to be called on this specific argList, in that it will first 289 | perform any necessary conversions and gather arguments up into "rest" 290 | parameters as needed. 291 | 292 | Thus, in the case that arguments `a0`,`a1`,`a2` (say) do match one of 293 | the signatures of this typed function `fn`, then `fn(a0, a1, a2)` 294 | (in a context in which `this` will be, say, `t`) does exactly the same 295 | thing as 296 | 297 | `typed.resolve(fn, [a0,a1,a2]).implementation.apply(t, [a0,a1,a2])`. 298 | 299 | But `resolve` is useful if you want to interpose any other operation 300 | (such as bookkeeping or additional custom error checking) between 301 | signature selection and execution dispatch. 302 | 303 | - `typed.findSignature(fn: typed-function, signature: string | Array, options: object) : signature-object` 304 | 305 | Find the signature object (as returned by `typed.resolve` above), but 306 | based on the specification of a signature (given either as a 307 | comma-separated string of parameter types, or an Array of strings giving 308 | the parameter types), rather than based on an example argument list. 309 | 310 | The optional third argument, is a plain object giving options controlling 311 | the search. Currently, the only implemented option is `exact`, which if 312 | true (defaults to false), limits the search to exact type matches, 313 | i.e. signatures for which no conversion functions need to be called in 314 | order to apply the function. 315 | 316 | Throws an error if the signature is not found. 317 | 318 | - `typed.find(fn: typed-function, signature: string | Array, options: object) : function` 319 | 320 | Convenience method that returns just the implementation from the 321 | signature object produced by `typed.findSignature(fn, signature, options)`. 322 | 323 | For example: 324 | 325 | ```js 326 | var fn = typed(...); 327 | var f = typed.find(fn, ['number', 'string']); 328 | var f = typed.find(fn, 'number, string', 'exact'); 329 | ``` 330 | 331 | - `typed.referTo(...string, callback: (resolvedFunctions: ...function) => function)` 332 | 333 | Within the definition of a typed-function, resolve references to one or 334 | multiple signatures of the typed-function itself. This looks like: 335 | 336 | ``` 337 | typed.referTo(signature1, signature2, ..., function callback(fn1, fn2, ...) { 338 | // ... use the resolved signatures fn1, fn2, ... 339 | }); 340 | ``` 341 | 342 | Example usage: 343 | 344 | ```js 345 | const fn = typed({ 346 | 'number': function (value) { 347 | return 'Input was a number: ' + value; 348 | }, 349 | 'boolean': function (value) { 350 | return 'Input was a boolean: ' + value; 351 | }, 352 | 'string': typed.referTo('number', 'boolean', (fnNumber, fnBoolean) => { 353 | return function fnString(value) { 354 | // here we use the signatures of the typed-function directly: 355 | if (value === 'true') { 356 | return fnBoolean(true); 357 | } 358 | if (value === 'false') { 359 | return fnBoolean(false); 360 | } 361 | return fnNumber(parseFloat(value)); 362 | } 363 | }) 364 | }); 365 | ``` 366 | 367 | See also `typed.referToSelf(callback)`. 368 | 369 | - `typed.referToSelf(callback: (self) => function)` 370 | 371 | Refer to the typed-function itself. This can be used for recursive calls. 372 | Calls to self will incur the overhead of fully re-dispatching the 373 | typed-function. If the signature that needs to be invoked is already known, 374 | you can use `typed.referTo(...)` instead for better performance. 375 | 376 | > In `typed-function@2` it was possible to use `this(...)` to reference the typed-function itself. In `typed-function@v3`, such usage is replaced with the `typed.referTo(...)` and `typed.referToSelf(...)` methods. Typed-functions are unbound in `typed-function@v3` and can be bound to another context if needed. 377 | 378 | - `typed.isTypedFunction(entity: any): boolean` 379 | 380 | Return true if the given entity appears to be a typed function 381 | (created by any instance of typed-function), and false otherwise. It 382 | tests for the presence of a particular property on the entity, 383 | and so could be deceived by another object with the same property, although 384 | the property is chosen so that's unlikely to happen unintentionally. 385 | 386 | - `typed.addType(type: {name: string, test: function, [, beforeObjectTest=true]): void` 387 | 388 | Add a new type. A type object contains a name and a test function. 389 | The order of the types determines in which order function arguments are 390 | type-checked, so for performance it's important to put the most used types 391 | first. Also, if one type is contained in another, it should likely precede 392 | it in the type order so that it won't be masked in type testing. 393 | 394 | Example: 395 | 396 | ```js 397 | function Person(...) { 398 | ... 399 | } 400 | 401 | Person.prototype.isPerson = true; 402 | 403 | typed.addType({ 404 | name: 'Person', 405 | test: function (x) { 406 | return x && x.isPerson === true; 407 | } 408 | }); 409 | ``` 410 | 411 | By default, the new type will be inserted before the `Object` test 412 | because the `Object` test also matches arrays and classes and hence 413 | `typed-function` would never reach the new type. When `beforeObjectTest` 414 | is `false`, the new type will be added at the end of all tests. 415 | 416 | - `typed.addTypes(types: TypeDef[] [, before = 'any']): void` 417 | 418 | Adds an list of new types. Each entry of the `types` array is an object 419 | like the `type` argument to `typed.addType`. The optional `before` argument 420 | is similar to `typed.addType` as well, except it should be the name of an 421 | arbitrary type that has already been added (rather than just a boolean flag) 422 | 423 | - `typed.clear(): void` 424 | 425 | Removes all types and conversions from the typed instance. Note that any 426 | typed-functions created before a call to `clear` will still operate, but 427 | they may prouce unintelligible messages in case of type mismatch errors. 428 | 429 | - `typed.addConversion(conversion: {from: string, to: string, convert: function}, options?: { override: boolean }) : void` 430 | 431 | Add a new conversion. 432 | 433 | ```js 434 | typed.addConversion({ 435 | from: 'boolean', 436 | to: 'number', 437 | convert: function (x) { 438 | return +x; 439 | }); 440 | ``` 441 | 442 | Note that any typed functions created before this conversion is added will 443 | not have their arguments undergo this new conversion automatically, so it is 444 | best to add all of your desired automatic conversions before defining any 445 | typed functions. 446 | 447 | - `typed.addConversions(conversions: ConversionDef[], options?: { override: boolean }): void` 448 | 449 | Convenience method that adds a list of conversions. Each element in the 450 | `conversions` array should be an object like the `conversion` argument of 451 | `typed.addConversion`. 452 | 453 | - `typed.removeConversion(conversion: ConversionDef): void` 454 | 455 | Removes a single existing conversion. An error is thrown if there is no 456 | conversion from and to the given types with a strictly equal convert 457 | function as supplied in this call. 458 | 459 | - `typed.clearConversions(): void` 460 | 461 | Removes all conversions from the typed instance (leaving the types alone). 462 | 463 | - `typed.createError(name: string, args: Array., signatures: Array.): TypeError` 464 | 465 | Generates a custom error object reporting the problem with calling 466 | the typed function of the given `name` with the given `signatures` on the 467 | actual arguments `args`. Note the error object has an extra property `data` 468 | giving the details of the problem. This method is primarily useful in 469 | writing your own handler for a type mismatch (see the `typed.onMismatch` 470 | property below), in case you have tried to recover but end up deciding 471 | you want to throw the error that the default handler would have. 472 | 473 | ### Properties 474 | 475 | - `typed.onMismatch: function` 476 | 477 | The handler called when a typed-function call fails to match with any 478 | of its signatures. The handler is called with three arguments: the name 479 | of the typed function being called, the actual argument list, and an array 480 | of the signatures for the typed function being called. (Each signature is 481 | an object with property 'signature' giving the actual signature and\ 482 | property 'fn' giving the raw function for that signature.) The default 483 | value of `onMismatch` is `typed.throwMismatchError`. 484 | 485 | This can be useful if you have a collection of functions and have common 486 | behavior for any invalid call. For example, you might just want to log 487 | the problem and continue: 488 | 489 | ``` 490 | const myErrorLog = []; 491 | typed.onMismatch = (name, args, signatures) => { 492 | myErrorLog.push(`Invalid call of ${name} with ${args.length} arguments.`); 493 | return null; 494 | }; 495 | typed.sqrt(9); // assuming definition as above, will return 3 496 | typed.sqrt([]); // no error will be thrown; will return null. 497 | console.log(`There have been ${myErrorLog.length} invalid calls.`) 498 | ``` 499 | 500 | Note that there is only one `onMismatch` handler at a time; assigning a 501 | new value discards the previous handler. To restore the default behavior, 502 | just assign `typed.onMismatch = typed.throwMismatchError`. 503 | 504 | Finally note that this handler fires whenever _any_ typed function call 505 | does not match any of its signatures. You can in effect define such a 506 | "handler" for a _single_ typed function by simply specifying an 507 | implementation for the `...` signature: 508 | 509 | ``` 510 | const lenOrNothing = typed({ 511 | string: s => s.length, 512 | '...': () => 0 513 | }); 514 | console.log(lenOrNothing('Hello, world!')) // Output: 13 515 | console.log(lenOrNothing(57, 'varieties')) // Output: 0 516 | ``` 517 | 518 | - `typed.warnAgainstDeprecatedThis: boolean` 519 | 520 | Since `typed-function` v3, self-referencing a typed function using 521 | `this(...)` or `this.signatures` has been deprecated and replaced with 522 | the functions `typed.referTo` and `typed.referToSelf`. By default, all 523 | function bodies will be scanned against this deprecated usage pattern and 524 | an error will be thrown when encountered. To disable this validation step, 525 | change this option to `false`. 526 | 527 | ### Recursion 528 | 529 | The `this` keyword can be used to self-reference the typed-function: 530 | 531 | ```js 532 | var sqrt = typed({ 533 | 'number': function (value) { 534 | return Math.sqrt(value); 535 | }, 536 | 'string': function (value) { 537 | // on the following line we self reference the typed-function using "this" 538 | return this(parseInt(value, 10)); 539 | } 540 | }); 541 | 542 | // use the typed function 543 | console.log(sqrt('9')); // output: 3 544 | ``` 545 | 546 | 547 | ## Roadmap 548 | 549 | ### Version 4 550 | 551 | - Extend function signatures: 552 | - Optional arguments like `'[number], array'` or like `number=, array` 553 | - Nullable arguments like `'?Object'` 554 | - Allow conversions to fail (for example string to number is not always 555 | possible). Call this `fallible` or `optional`? 556 | 557 | ### Version 5 558 | 559 | - Extend function signatures: 560 | - Constants like `'"linear" | "cubic"'`, `'0..10'`, etc. 561 | - Object definitions like `'{name: string, age: number}'` 562 | - Object definitions like `'Object.'` 563 | - Array definitions like `'Array.'` 564 | - Improve performance of both generating a typed function as well as 565 | the performance and memory footprint of a typed function. 566 | 567 | 568 | ## Test 569 | 570 | To test the library, run: 571 | 572 | npm test 573 | 574 | 575 | ## Code style and linting 576 | 577 | The library is using the [standardjs](https://standardjs.com/) coding style. 578 | 579 | To test the code style, run: 580 | 581 | npm run lint 582 | 583 | To automatically fix most of the styling issues, run: 584 | 585 | npm run format 586 | 587 | 588 | ## Publish 589 | 590 | 1. Describe the changes in `HISTORY.md` 591 | 2. Increase the version number in `package.json` 592 | 3. Test and build: 593 | ``` 594 | npm install 595 | npm run build-and-test 596 | ``` 597 | 4. Verify whether the generated output works correctly by opening 598 | `./test/browserEsmBuild.html` in your browser. 599 | 5. Commit the changes 600 | 6. Merge `develop` into `master`, and push `master` 601 | 7. Create a git tag, and push this 602 | 8. publish the library: 603 | ``` 604 | npm publish 605 | ``` 606 | -------------------------------------------------------------------------------- /babel-cjs.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": "> 0.25%, not dead", 7 | "modules": "commonjs" 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": "> 0.25%, not dead", 7 | "modules": false 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /benchmark/benchmark.mjs: -------------------------------------------------------------------------------- 1 | // 2 | // typed-function benchmark 3 | // 4 | // WARNING: be careful, these are micro-benchmarks, which can only be used 5 | // to get an indication of the performance. Real performance and 6 | // bottlenecks should be assessed in real world applications, 7 | // not in micro-benchmarks. 8 | // 9 | // Before running, make sure you've installed the needed packages which 10 | // are defined in the devDependencies of the project. 11 | // 12 | // To create a bundle for testing in a browser: 13 | // 14 | // browserify -o benchmark/benchmark.bundle.js benchmark/benchmark.js 15 | // 16 | import assert from 'assert'; 17 | import Benchmark from 'benchmark'; 18 | import padRight from 'pad-right'; 19 | import typed from '../src/typed-function.mjs'; 20 | 21 | // expose on window when using bundled in a browser 22 | if (typeof window !== 'undefined') { 23 | window['Benchmark'] = Benchmark; 24 | } 25 | 26 | function vanillaAdd(x, y) { 27 | return x + y; 28 | } 29 | 30 | const typedAdd = typed('add', { 31 | 'number, number': (x, y) => x + y, 32 | 'boolean, boolean': (x, y) => x + y, 33 | 'Date, Date': (x, y) => x + y, 34 | 'string, string': (x, y) => x + y 35 | }); 36 | 37 | assert.strictEqual(vanillaAdd(2,3), 5); 38 | assert.strictEqual(typedAdd(2, 3), 5); 39 | assert.strictEqual(typedAdd('hello', 'world'), 'helloworld'); 40 | assert.throws(function () { typedAdd(1) }, /TypeError/) 41 | assert.throws(function () { typedAdd(1,2,3) }, /TypeError/) 42 | 43 | const typed2 = createTyped(11, 10) 44 | 45 | const typed1Signature0Conversions = createTyped1Signature0Conversions(typed2) 46 | assert.strictEqual(typed1Signature0Conversions('Type0', 'Type0'), 'Result:Type0:Type0') 47 | 48 | const typed10Signatures0Conversions = createTyped10Signatures0Conversions(typed2) 49 | assert.strictEqual(typed10Signatures0Conversions('Type0', 'Type0'), 'Result:Type0:Type0') 50 | assert.strictEqual(typed10Signatures0Conversions('Type7', 'Type7'), 'Result:Type7:Type7') 51 | 52 | const typed1Signature10Conversions = createTyped1Signature10Conversions(typed2) 53 | assert.strictEqual(typed1Signature10Conversions('Type0', 'Type0'), 'Result:Type0->TypeBase:Type0') 54 | assert.strictEqual(typed1Signature10Conversions('Type7', 'Type0'), 'Result:Type7->TypeBase:Type0') 55 | 56 | const typed10Signatures10Conversions = createTyped10Signatures10Conversions(typed2) 57 | assert.strictEqual(typed10Signatures10Conversions('TypeBase', 'Type0'), 'Result:TypeBase:Type0') 58 | assert.strictEqual(typed10Signatures10Conversions('Type7', 'Type0'), 'Result:Type7->TypeBase:Type0') 59 | assert.strictEqual(typed10Signatures10Conversions('Type7', 'Type5'), 'Result:Type7->TypeBase:Type5') 60 | 61 | const paramsCount = 20 62 | const manyParams = Array(paramsCount).fill('Type0') 63 | const typed1SignatureManyParams = createTyped1SignatureManyParams(typed2, paramsCount) 64 | assert.strictEqual(typed1SignatureManyParams.apply(null, manyParams),'Result:' + manyParams.join(':')) 65 | 66 | const suite = new Benchmark.Suite('typed-function'); 67 | 68 | let result = 0; 69 | suite 70 | // compare vanilla vs typed execution 71 | .add(pad('execute: vanillaAdd'), function() { 72 | result += vanillaAdd(result, 4); 73 | result += vanillaAdd(String(result), 'world').length; 74 | }) 75 | .add(pad('execute: typedAdd'), function() { 76 | result += typedAdd(result, 4); 77 | result += typedAdd(String(result), 'world').length; 78 | }) 79 | 80 | // see execution time of various typed functions 81 | .add(pad('execute: 1 signature, 0 conversions'), function() { 82 | typed1Signature0Conversions('Type0', 'Type0') 83 | }) 84 | .add(pad('execute: 10 signatures, 0 conversions'), function() { 85 | typed10Signatures0Conversions('Type0', 'Type0') 86 | }) 87 | .add(pad('execute: 1 signatures, 10 conversions'), function() { 88 | typed1Signature10Conversions('Type0', 'Type0') 89 | }) 90 | .add(pad('execute: 10 signatures, 10 conversions'), function() { 91 | typed10Signatures10Conversions('Type0', 'Type0') 92 | }) 93 | .add(pad(`execute: 1 signature, ${paramsCount} params`), function() { 94 | typed1SignatureManyParams.apply(null, manyParams) 95 | }) 96 | 97 | // see creation time of various typed functions 98 | .add(pad('create: 1 signature, 0 conversions'), function() { 99 | createTyped1Signature0Conversions(typed2) 100 | }) 101 | .add(pad('create: 10 signatures, 0 conversions'), function() { 102 | createTyped10Signatures0Conversions(typed2) 103 | }) 104 | .add(pad('create: 1 signatures, 10 conversions'), function() { 105 | createTyped1Signature10Conversions(typed2) 106 | }) 107 | .add(pad('create: 10 signatures, 10 conversions'), function() { 108 | createTyped10Signatures10Conversions(typed2) 109 | }) 110 | .add(pad(`create: 1 signature, ${paramsCount} params`), function() { 111 | createTyped1SignatureManyParams(typed2, paramsCount) 112 | }) 113 | 114 | // run and output stuff 115 | .on('cycle', function(event) { 116 | console.log(String(event.target)); 117 | }) 118 | .on('complete', function() { 119 | console.log('First typed universe created', typed.createCount, 'functions') 120 | console.log('typed2 universe created', typed2.createCount, 'functions') 121 | }) 122 | .run(); 123 | 124 | function createTyped1Signature0Conversions(typed) { 125 | return typed('1Signature', { 126 | 'Type0,Type0': (a, b) => 'Result:' + a + ':' + b 127 | }) 128 | } 129 | 130 | function createTyped10Signatures0Conversions(typed) { 131 | const count = 10 132 | 133 | const signatures = {} 134 | for (let t = 0; t < count; t++) { 135 | signatures[`Type${t}, Type${t}`] = (a, b) => 'Result:' + a + ':' + b 136 | } 137 | 138 | return typed('10Signatures', signatures) 139 | } 140 | 141 | function createTyped1Signature10Conversions(typed) { 142 | return typed('1Signature10conversions', { 143 | 'TypeBase, Type0': (a, b) => 'Result:' + a + ':' + b 144 | }) 145 | } 146 | 147 | function createTyped10Signatures10Conversions(typed) { 148 | const count = 10 149 | const signatures = {} 150 | for (let t = 0; t < count; t++) { 151 | signatures[`TypeBase, Type${t}`] = (a, b) => 'Result:' + a + ':' + b 152 | } 153 | 154 | return typed('10Signatures10conversions', signatures) 155 | } 156 | 157 | function createTyped1SignatureManyParams(typed, paramsCount) { 158 | const signatureStr = Array(paramsCount).fill('Type0') 159 | 160 | return typed(`1Signature${paramsCount}Params`, { 161 | [signatureStr]: (...args) => 'Result:' + args.join(':') 162 | }) 163 | } 164 | 165 | function createTyped(typeCount, conversionCount) { 166 | const newTyped = typed.create() 167 | newTyped.types = [] 168 | newTyped.conversions = [] 169 | 170 | const baseName = 'TypeBase' 171 | newTyped.addType({ 172 | name: baseName, 173 | test: function (value) { 174 | return typeof value === 'string' && value === baseName 175 | } 176 | }) 177 | 178 | for (let t = 0; t < typeCount; t++) { 179 | const name = 'Type' + t 180 | 181 | newTyped.addType({ 182 | name, 183 | test: function (value) { 184 | return typeof value === 'string' && value === name 185 | } 186 | }) 187 | } 188 | 189 | for (let c = 0; c < conversionCount; c++) { 190 | newTyped.addConversion({ 191 | from: 'Type' + c, 192 | to: baseName, 193 | convert: function (value) { 194 | return value + '->' + baseName; 195 | } 196 | }) 197 | } 198 | 199 | return newTyped 200 | } 201 | 202 | function pad (text) { 203 | return padRight(text, 40, ' '); 204 | } 205 | -------------------------------------------------------------------------------- /examples/any_type.mjs: -------------------------------------------------------------------------------- 1 | import typed from '../src/typed-function.mjs'; 2 | 3 | // create a typed function with an any type argument 4 | const log = typed({ 5 | 'string, any': function (event, data) { 6 | console.log('event: ' + event + ', data: ' + JSON.stringify(data)); 7 | } 8 | }); 9 | 10 | // use the typed function 11 | log('start', {count: 2}); // output: 'event: start, data: {"count":2}' 12 | log('end', 'success!'); // output: 'event: start, data: "success!" 13 | -------------------------------------------------------------------------------- /examples/basic_usage.mjs: -------------------------------------------------------------------------------- 1 | import typed from '../src/typed-function.mjs'; 2 | 3 | // create a typed function 4 | var fn1 = typed({ 5 | 'number, string': function (a, b) { 6 | return 'a is a number, b is a string'; 7 | } 8 | }); 9 | 10 | // create a typed function with multiple types per argument (type union) 11 | var fn2 = typed({ 12 | 'string, number | boolean': function (a, b) { 13 | return 'a is a string, b is a number or a boolean'; 14 | } 15 | }); 16 | 17 | // create a typed function with any type argument 18 | var fn3 = typed({ 19 | 'string, any': function (a, b) { 20 | return 'a is a string, b can be anything'; 21 | } 22 | }); 23 | 24 | // create a typed function with multiple signatures 25 | var fn4 = typed({ 26 | 'number': function (a) { 27 | return 'a is a number'; 28 | }, 29 | 'number, boolean': function (a, b) { 30 | return 'a is a number, b is a boolean'; 31 | }, 32 | 'number, number': function (a, b) { 33 | return 'a is a number, b is a number'; 34 | } 35 | }); 36 | 37 | // create a typed function from a plain function with signature 38 | function fnPlain(a, b) { 39 | return 'a is a number, b is a string'; 40 | } 41 | fnPlain.signature = 'number, string'; 42 | var fn5 = typed(fnPlain); 43 | 44 | // use the functions 45 | console.log(fn1(2, 'foo')); // outputs 'a is a number, b is a string' 46 | console.log(fn4(2)); // outputs 'a is a number' 47 | 48 | // calling the function with a non-supported type signature will throw an error 49 | try { 50 | fn2('hello', 'world'); 51 | } 52 | catch (err) { 53 | console.log(err.toString()); 54 | // outputs: TypeError: Unexpected type of argument. 55 | // Expected: number or boolean, actual: string, index: 1. 56 | } 57 | -------------------------------------------------------------------------------- /examples/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | typed-function | basic usage 5 | 6 | 7 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/custom_type.mjs: -------------------------------------------------------------------------------- 1 | import typed from '../src/typed-function.mjs'; 2 | 3 | // create a prototype 4 | function Person(params) { 5 | this.name = params.name; 6 | this.age = params.age; 7 | } 8 | 9 | // register a test for this new type 10 | typed.addType({ 11 | name: 'Person', 12 | test: function (x) { 13 | return x instanceof Person; 14 | } 15 | }); 16 | 17 | // create a typed function 18 | var stringify = typed({ 19 | 'Person': function (person) { 20 | return JSON.stringify(person); 21 | } 22 | }); 23 | 24 | // use the function 25 | var person = new Person({name: 'John', age: 28}); 26 | 27 | console.log(stringify(person)); 28 | // outputs: '{"name":"John","age":28}' 29 | 30 | // calling the function with a non-supported type signature will throw an error 31 | try { 32 | stringify('ooops'); 33 | } 34 | catch (err) { 35 | console.log('Wrong input will throw an error:'); 36 | console.log(' ' + err.toString()); 37 | // outputs: TypeError: Unexpected type of argument (expected: Person, 38 | // actual: string, index: 0) 39 | } 40 | -------------------------------------------------------------------------------- /examples/merge_plain_functions.mjs: -------------------------------------------------------------------------------- 1 | import typed from '../src/typed-function.mjs'; 2 | 3 | // create a couple of plain functions with a signature 4 | function fn1 (a) { 5 | return a + a; 6 | } 7 | fn1.signature = 'number'; 8 | 9 | function fn2 (a) { 10 | var value = +a; 11 | return value + value; 12 | } 13 | fn2.signature = 'string'; 14 | 15 | // merge multiple typed functions 16 | var fn3 = typed('fn3', fn1, fn2); 17 | 18 | // use merged function 19 | console.log(fn3(2)); // outputs 4 20 | console.log(fn3('3')); // outputs 6 21 | -------------------------------------------------------------------------------- /examples/merge_typed_functions.mjs: -------------------------------------------------------------------------------- 1 | import typed from '../src/typed-function.mjs'; 2 | 3 | // create a couple of typed functions 4 | var fn1 = typed({ 5 | 'number': function (a) { 6 | return a + a; 7 | } 8 | }); 9 | var fn2 = typed({ 10 | 'string': function (a) { 11 | var value = +a; 12 | return value + value; 13 | } 14 | }); 15 | 16 | // merge multiple typed functions 17 | var fn3 = typed(fn1, fn2); 18 | 19 | // use merged function 20 | console.log(fn3(2)); // outputs 4 21 | console.log(fn3('3')); // outputs 6 22 | -------------------------------------------------------------------------------- /examples/multiple_signatures.mjs: -------------------------------------------------------------------------------- 1 | import typed from '../src/typed-function.mjs'; 2 | 3 | // create a typed function with multiple signatures 4 | var fn = typed({ 5 | 'number': function (a) { 6 | return 'a is a number'; 7 | }, 8 | 'number, boolean': function (a, b) { 9 | return 'a is a number, b is a boolean'; 10 | }, 11 | 'number, number': function (a, b) { 12 | return 'a is a number, b is a number'; 13 | } 14 | }); 15 | 16 | // use the function 17 | console.log(fn(2, true)); // outputs 'a is a number, b is a boolean' 18 | console.log(fn(2)); // outputs 'a is a number' 19 | -------------------------------------------------------------------------------- /examples/recursion.mjs: -------------------------------------------------------------------------------- 1 | import typed from '../src/typed-function.mjs'; 2 | 3 | // create a typed function that invokes itself 4 | var sqrt = typed({ 5 | 'number': function (value) { 6 | return Math.sqrt(value); 7 | }, 8 | 'string': typed.referToSelf(self => function (value) { 9 | return self(parseInt(value, 10)); 10 | }) 11 | }); 12 | 13 | // use the typed function 14 | console.log(sqrt("9")); // output: 3 15 | -------------------------------------------------------------------------------- /examples/rest_parameters.mjs: -------------------------------------------------------------------------------- 1 | import typed from '../src/typed-function.mjs'; 2 | 3 | // create a typed function with a variable number of arguments 4 | var sum = typed({ 5 | '...number': function (values) { 6 | var sum = 0; 7 | for (var i = 0; i < values.length; i++) { 8 | sum += values[i]; 9 | } 10 | return sum; 11 | } 12 | }); 13 | 14 | // use the typed function 15 | console.log(sum(2, 3)); // output: 5 16 | console.log(sum(2, 3, 1, 2)); // output: 8 17 | -------------------------------------------------------------------------------- /examples/type_conversion.mjs: -------------------------------------------------------------------------------- 1 | import typed from '../src/typed-function.mjs'; 2 | 3 | // define type conversions that we want to support order is important. 4 | typed.addConversions([ 5 | { 6 | from: 'boolean', 7 | to: 'number', 8 | convert: function (x) { 9 | return +x; 10 | } 11 | }, 12 | { 13 | from: 'boolean', 14 | to: 'string', 15 | convert: function (x) { 16 | return x + ''; 17 | } 18 | }, 19 | { 20 | from: 'number', 21 | to: 'string', 22 | convert: function (x) { 23 | return x + ''; 24 | } 25 | } 26 | ]); 27 | 28 | // create a typed function with multiple signatures 29 | // 30 | // where possible, the created function will automatically convert booleans to 31 | // numbers or strings, and convert numbers to strings. 32 | // 33 | // note that the length property is only available on strings, and the toFixed 34 | // function only on numbers, so this requires the right type of argument else 35 | // the function will throw an exception. 36 | var fn = typed({ 37 | 'string': function (name) { 38 | return 'Name: ' + name + ', length: ' + name.length; 39 | }, 40 | 'string, number': function (name, value) { 41 | return 'Name: ' + name + ', length: ' + name.length + ', value: ' + value.toFixed(3); 42 | } 43 | }); 44 | 45 | // use the function the regular way 46 | console.log(fn('foo')); // outputs 'Name: foo, length: 3' 47 | console.log(fn('foo', 2/3)); // outputs 'Name: foo, length: 3, value: 0.667' 48 | 49 | // calling the function with non-supported but convertible types 50 | // will work just fine: 51 | console.log(fn(false)); // outputs 'Name: false, length: 5' 52 | console.log(fn('foo', true)); // outputs 'Name: foo, length: 3, value: 1.000' 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typed-function", 3 | "version": "4.2.1", 4 | "description": "Type checking for JavaScript functions", 5 | "author": "Jos de Jong (https://github.com/josdejong)", 6 | "contributors": [ 7 | "Glen Whitney (https://github.com/gwhitney)", 8 | "Luke Gumbley (https://github.com/luke-gumbley)" 9 | ], 10 | "homepage": "https://github.com/josdejong/typed-function", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/josdejong/typed-function.git" 14 | }, 15 | "keywords": [ 16 | "typed", 17 | "function", 18 | "arguments", 19 | "compose", 20 | "types" 21 | ], 22 | "type": "module", 23 | "main": "lib/umd/typed-function.js", 24 | "module": "lib/esm/typed-function.mjs", 25 | "browser": "lib/umd/typed-function.js", 26 | "scripts": { 27 | "test": "mocha test --recursive", 28 | "test:lib": "mocha test test-lib --recursive", 29 | "build": "npm-run-all build:**", 30 | "build:clean": "del-cli lib", 31 | "build:esm": "babel src --out-dir lib/esm --out-file-extension .mjs --source-maps --config-file ./babel.config.json", 32 | "build:umd": "rollup lib/esm/typed-function.mjs --format umd --name 'typed' --sourcemap --output.file lib/umd/typed-function.js && cpy tools/cjs/package.json lib/umd --flat", 33 | "build-and-test": "npm run lint && npm run build && npm run test:lib", 34 | "lint": "eslint --cache src/**/*.mjs test/**/*.mjs test-lib/**/*.mjs", 35 | "format": "npm run lint -- --fix", 36 | "coverage": "c8 --reporter=lcov --reporter=text-summary mocha test --recursive && echo \"\nCoverage report is available at ./coverage/lcov-report/index.html\"", 37 | "prepublishOnly": "npm run build-and-test" 38 | }, 39 | "engines": { 40 | "node": ">= 18" 41 | }, 42 | "devDependencies": { 43 | "@babel/cli": "7.24.6", 44 | "@babel/preset-env": "7.24.6", 45 | "benchmark": "2.1.4", 46 | "c8": "9.1.0", 47 | "cpy-cli": "5.0.0", 48 | "del-cli": "5.1.0", 49 | "eslint": "8.56.0", 50 | "eslint-config-standard": "17.1.0", 51 | "eslint-plugin-import": "2.29.1", 52 | "eslint-plugin-n": "16.6.2", 53 | "eslint-plugin-promise": "6.2.0", 54 | "mocha": "10.4.0", 55 | "npm-run-all": "4.1.5", 56 | "pad-right": "0.2.2", 57 | "rollup": "4.18.0" 58 | }, 59 | "files": [ 60 | "README.md", 61 | "LICENSE.md", 62 | "lib" 63 | ], 64 | "license": "MIT" 65 | } -------------------------------------------------------------------------------- /src/typed-function.mjs: -------------------------------------------------------------------------------- 1 | function ok () { 2 | return true 3 | } 4 | 5 | function notOk () { 6 | return false 7 | } 8 | 9 | function undef () { 10 | return undefined 11 | } 12 | 13 | const NOT_TYPED_FUNCTION = 'Argument is not a typed-function.' 14 | 15 | /** 16 | * @typedef {{ 17 | * params: Param[], 18 | * fn: function, 19 | * test: function, 20 | * implementation: function 21 | * }} Signature 22 | * 23 | * @typedef {{ 24 | * types: Type[], 25 | * hasAny: boolean, 26 | * hasConversion: boolean, 27 | * restParam: boolean 28 | * }} Param 29 | * 30 | * @typedef {{ 31 | * name: string, 32 | * typeIndex: number, 33 | * test: function, 34 | * isAny: boolean, 35 | * conversion?: ConversionDef, 36 | * conversionIndex: number, 37 | * }} Type 38 | * 39 | * @typedef {{ 40 | * from: string, 41 | * to: string, 42 | * convert: function (*) : * 43 | * }} ConversionDef 44 | * 45 | * @typedef {{ 46 | * name: string, 47 | * test: function(*) : boolean, 48 | * isAny?: boolean 49 | * }} TypeDef 50 | */ 51 | 52 | /** 53 | * @returns {() => function} 54 | */ 55 | function create () { 56 | // data type tests 57 | 58 | /** 59 | * Returns true if the argument is a non-null "plain" object 60 | */ 61 | function isPlainObject (x) { 62 | return typeof x === 'object' && x !== null && x.constructor === Object 63 | } 64 | 65 | const _types = [ 66 | { name: 'number', test: function (x) { return typeof x === 'number' } }, 67 | { name: 'string', test: function (x) { return typeof x === 'string' } }, 68 | { name: 'boolean', test: function (x) { return typeof x === 'boolean' } }, 69 | { name: 'Function', test: function (x) { return typeof x === 'function' } }, 70 | { name: 'Array', test: Array.isArray }, 71 | { name: 'Date', test: function (x) { return x instanceof Date } }, 72 | { name: 'RegExp', test: function (x) { return x instanceof RegExp } }, 73 | { name: 'Object', test: isPlainObject }, 74 | { name: 'null', test: function (x) { return x === null } }, 75 | { name: 'undefined', test: function (x) { return x === undefined } } 76 | ] 77 | 78 | const anyType = { 79 | name: 'any', 80 | test: ok, 81 | isAny: true 82 | } 83 | 84 | // Data structures to track the types. As these are local variables in 85 | // create(), each typed universe will get its own copy, but the variables 86 | // will only be accessible through the (closures of the) functions supplied 87 | // as properties of the typed object, not directly. 88 | // These will be initialized in clear() below 89 | let typeMap // primary store of all types 90 | let typeList // Array of just type names, for the sake of ordering 91 | 92 | // And similar data structures for the type conversions: 93 | let nConversions = 0 94 | // the actual conversions are stored on a property of the destination types 95 | 96 | // This is a temporary object, will be replaced with a function at the end 97 | let typed = { createCount: 0 } 98 | 99 | /** 100 | * Takes a type name and returns the corresponding official type object 101 | * for that type. 102 | * 103 | * @param {string} typeName 104 | * @returns {TypeDef} type 105 | */ 106 | function findType (typeName) { 107 | const type = typeMap.get(typeName) 108 | if (type) { 109 | return type 110 | } 111 | // Remainder is error handling 112 | let message = 'Unknown type "' + typeName + '"' 113 | const name = typeName.toLowerCase() 114 | let otherName 115 | for (otherName of typeList) { 116 | if (otherName.toLowerCase() === name) { 117 | message += '. Did you mean "' + otherName + '" ?' 118 | break 119 | } 120 | } 121 | throw new TypeError(message) 122 | } 123 | 124 | /** 125 | * Adds an array `types` of type definitions to this typed instance. 126 | * Each type definition should be an object with properties: 127 | * 'name' - a string giving the name of the type; 'test' - function 128 | * returning a boolean that tests membership in the type; and optionally 129 | * 'isAny' - true only for the 'any' type. 130 | * 131 | * The second optional argument, `before`, gives the name of a type that 132 | * these types should be added before. The new types are added in the 133 | * order specified. 134 | * @param {TypeDef[]} types 135 | * @param {string | boolean} [beforeSpec='any'] before 136 | */ 137 | function addTypes (types, beforeSpec = 'any') { 138 | const beforeIndex = beforeSpec 139 | ? findType(beforeSpec).index 140 | : typeList.length 141 | const newTypes = [] 142 | for (let i = 0; i < types.length; ++i) { 143 | if (!types[i] || typeof types[i].name !== 'string' || 144 | typeof types[i].test !== 'function') { 145 | throw new TypeError('Object with properties {name: string, test: function} expected') 146 | } 147 | const typeName = types[i].name 148 | if (typeMap.has(typeName)) { 149 | throw new TypeError('Duplicate type name "' + typeName + '"') 150 | } 151 | newTypes.push(typeName) 152 | typeMap.set(typeName, { 153 | name: typeName, 154 | test: types[i].test, 155 | isAny: types[i].isAny, 156 | index: beforeIndex + i, 157 | conversionsTo: [] // Newly added type can't have any conversions to it 158 | }) 159 | } 160 | // update the typeList 161 | const affectedTypes = typeList.slice(beforeIndex) 162 | typeList = 163 | typeList.slice(0, beforeIndex).concat(newTypes).concat(affectedTypes) 164 | // Fix the indices 165 | for (let i = beforeIndex + newTypes.length; i < typeList.length; ++i) { 166 | typeMap.get(typeList[i]).index = i 167 | } 168 | } 169 | 170 | /** 171 | * Removes all types and conversions from this typed instance. 172 | * May cause previously constructed typed-functions to throw 173 | * strange errors when they are called with types that do not 174 | * match any of their signatures. 175 | */ 176 | function clear () { 177 | typeMap = new Map() 178 | typeList = [] 179 | nConversions = 0 180 | addTypes([anyType], false) 181 | } 182 | 183 | // initialize the types to the default list 184 | clear() 185 | addTypes(_types) 186 | 187 | /** 188 | * Removes all conversions, leaving the types alone. 189 | */ 190 | function clearConversions () { 191 | let typeName 192 | for (typeName of typeList) { 193 | typeMap.get(typeName).conversionsTo = [] 194 | } 195 | nConversions = 0 196 | } 197 | 198 | /** 199 | * Find the type names that match a value. 200 | * @param {*} value 201 | * @return {string[]} Array of names of types for which 202 | * the type test matches the value. 203 | */ 204 | function findTypeNames (value) { 205 | const matches = typeList.filter(name => { 206 | const type = typeMap.get(name) 207 | return !type.isAny && type.test(value) 208 | }) 209 | if (matches.length) { 210 | return matches 211 | } 212 | return ['any'] 213 | } 214 | 215 | /** 216 | * Check if an entity is a typed function created by any instance 217 | * @param {any} entity 218 | * @returns {boolean} 219 | */ 220 | function isTypedFunction (entity) { 221 | return entity && typeof entity === 'function' && 222 | '_typedFunctionData' in entity 223 | } 224 | 225 | /** 226 | * Find a specific signature from a (composed) typed function, for example: 227 | * 228 | * typed.findSignature(fn, ['number', 'string']) 229 | * typed.findSignature(fn, 'number, string') 230 | * typed.findSignature(fn, 'number,string', {exact: true}) 231 | * 232 | * This function findSignature will by default return the best match to 233 | * the given signature, possibly employing type conversions. 234 | * 235 | * The (optional) third argument is a plain object giving options 236 | * controlling the signature search. Currently the only implemented 237 | * option is `exact`: if specified as true (default is false), only 238 | * exact matches will be returned (i.e. signatures for which `fn` was 239 | * directly defined). Note that a (possibly different) type matching 240 | * `any`, or one or more instances of TYPE matching `...TYPE` are 241 | * considered exact matches in this regard, as no conversions are used. 242 | * 243 | * This function returns a "signature" object, as does `typed.resolve()`, 244 | * which is a plain object with four keys: `params` (the array of parameters 245 | * for this signature), `fn` (the originally supplied function for this 246 | * signature), `test` (a generated function that determines if an argument 247 | * list matches this signature, and `implementation` (the function to call 248 | * on a matching argument list, that performs conversions if necessary and 249 | * then calls the originally supplied function). 250 | * 251 | * @param {Function} fn A typed-function 252 | * @param {string | string[]} signature 253 | * Signature to be found, can be an array or a comma separated string. 254 | * @param {object} options Controls the signature search as documented 255 | * @return {{ params: Param[], fn: function, test: function, implementation: function }} 256 | * Returns the matching signature, or throws an error when no signature 257 | * is found. 258 | */ 259 | function findSignature (fn, signature, options) { 260 | if (!isTypedFunction(fn)) { 261 | throw new TypeError(NOT_TYPED_FUNCTION) 262 | } 263 | 264 | // Canonicalize input 265 | const exact = options && options.exact 266 | const stringSignature = Array.isArray(signature) 267 | ? signature.join(',') 268 | : signature 269 | const params = parseSignature(stringSignature) 270 | const canonicalSignature = stringifyParams(params) 271 | 272 | // First hope we get lucky and exactly match a signature 273 | if (!exact || canonicalSignature in fn.signatures) { 274 | // OK, we can check the internal signatures 275 | const match = 276 | fn._typedFunctionData.signatureMap.get(canonicalSignature) 277 | if (match) { 278 | return match 279 | } 280 | } 281 | 282 | // Oh well, we did not; so we have to go back and check the parameters 283 | // one by one, in order to catch things like `any` and rest params. 284 | // Note here we can assume there is at least one parameter, because 285 | // the empty signature would have matched successfully above. 286 | const nParams = params.length 287 | let remainingSignatures 288 | if (exact) { 289 | remainingSignatures = [] 290 | let name 291 | for (name in fn.signatures) { 292 | remainingSignatures.push(fn._typedFunctionData.signatureMap.get(name)) 293 | } 294 | } else { 295 | remainingSignatures = fn._typedFunctionData.signatures 296 | } 297 | for (let i = 0; i < nParams; ++i) { 298 | const want = params[i] 299 | const filteredSignatures = [] 300 | let possibility 301 | for (possibility of remainingSignatures) { 302 | const have = getParamAtIndex(possibility.params, i) 303 | if (!have || (want.restParam && !have.restParam)) { 304 | continue 305 | } 306 | if (!have.hasAny) { 307 | // have to check all of the wanted types are available 308 | const haveTypes = paramTypeSet(have) 309 | if (want.types.some(wtype => !haveTypes.has(wtype.name))) { 310 | continue 311 | } 312 | } 313 | // OK, this looks good 314 | filteredSignatures.push(possibility) 315 | } 316 | remainingSignatures = filteredSignatures 317 | if (remainingSignatures.length === 0) break 318 | } 319 | // Return the first remaining signature that was totally matched: 320 | let candidate 321 | for (candidate of remainingSignatures) { 322 | if (candidate.params.length <= nParams) { 323 | return candidate 324 | } 325 | } 326 | 327 | throw new TypeError('Signature not found (signature: ' + (fn.name || 'unnamed') + '(' + stringifyParams(params, ', ') + '))') 328 | } 329 | 330 | /** 331 | * Find the proper function to call for a specific signature from 332 | * a (composed) typed function, for example: 333 | * 334 | * typed.find(fn, ['number', 'string']) 335 | * typed.find(fn, 'number, string') 336 | * typed.find(fn, 'number,string', {exact: true}) 337 | * 338 | * This function find will by default return the best match to 339 | * the given signature, possibly employing type conversions (and returning 340 | * a function that will perform those conversions as needed). The 341 | * (optional) third argument is a plain object giving options contolling 342 | * the signature search. Currently only the option `exact` is implemented, 343 | * which defaults to "false". If `exact` is specified as true, then only 344 | * exact matches will be returned (i.e. signatures for which `fn` was 345 | * directly defined). Uses of `any` and `...TYPE` are considered exact if 346 | * no conversions are necessary to apply the corresponding function. 347 | * 348 | * @param {Function} fn A typed-function 349 | * @param {string | string[]} signature 350 | * Signature to be found, can be an array or a comma separated string. 351 | * @param {object} options Controls the signature match as documented 352 | * @return {function} 353 | * Returns the function to call for the given signature, or throws an 354 | * error if no match is found. 355 | */ 356 | function find (fn, signature, options) { 357 | return findSignature(fn, signature, options).implementation 358 | } 359 | 360 | /** 361 | * Convert a given value to another data type, specified by type name. 362 | * 363 | * @param {*} value 364 | * @param {string} typeName 365 | */ 366 | function convert (value, typeName) { 367 | // check conversion is needed 368 | const type = findType(typeName) 369 | if (type.test(value)) { 370 | return value 371 | } 372 | const conversions = type.conversionsTo 373 | if (conversions.length === 0) { 374 | throw new Error( 375 | 'There are no conversions to ' + typeName + ' defined.') 376 | } 377 | for (let i = 0; i < conversions.length; i++) { 378 | const fromType = findType(conversions[i].from) 379 | if (fromType.test(value)) { 380 | return conversions[i].convert(value) 381 | } 382 | } 383 | 384 | throw new Error('Cannot convert ' + value + ' to ' + typeName) 385 | } 386 | 387 | /** 388 | * Stringify parameters in a normalized way 389 | * @param {Param[]} params 390 | * @param {string} [','] separator 391 | * @return {string} 392 | */ 393 | function stringifyParams (params, separator = ',') { 394 | return params.map(p => p.name).join(separator) 395 | } 396 | 397 | /** 398 | * Parse a parameter, like "...number | boolean" 399 | * @param {string} param 400 | * @return {Param} param 401 | */ 402 | function parseParam (param) { 403 | const restParam = param.indexOf('...') === 0 404 | const types = (!restParam) 405 | ? param 406 | : (param.length > 3) 407 | ? param.slice(3) 408 | : 'any' 409 | 410 | const typeDefs = types.split('|').map(s => findType(s.trim())) 411 | 412 | let hasAny = false 413 | let paramName = restParam ? '...' : '' 414 | 415 | const exactTypes = typeDefs.map(function (type) { 416 | hasAny = type.isAny || hasAny 417 | paramName += type.name + '|' 418 | 419 | return { 420 | name: type.name, 421 | typeIndex: type.index, 422 | test: type.test, 423 | isAny: type.isAny, 424 | conversion: null, 425 | conversionIndex: -1 426 | } 427 | }) 428 | 429 | return { 430 | types: exactTypes, 431 | name: paramName.slice(0, -1), // remove trailing '|' from above 432 | hasAny, 433 | hasConversion: false, 434 | restParam 435 | } 436 | } 437 | 438 | /** 439 | * Expands a parsed parameter with the types available from currently 440 | * defined conversions. 441 | * @param {Param} param 442 | * @return {Param} param 443 | */ 444 | function expandParam (param) { 445 | const typeNames = param.types.map(t => t.name) 446 | const matchingConversions = availableConversions(typeNames) 447 | let hasAny = param.hasAny 448 | let newName = param.name 449 | 450 | const convertibleTypes = matchingConversions.map(function (conversion) { 451 | const type = findType(conversion.from) 452 | hasAny = type.isAny || hasAny 453 | newName += '|' + conversion.from 454 | 455 | return { 456 | name: conversion.from, 457 | typeIndex: type.index, 458 | test: type.test, 459 | isAny: type.isAny, 460 | conversion, 461 | conversionIndex: conversion.index 462 | } 463 | }) 464 | 465 | return { 466 | types: param.types.concat(convertibleTypes), 467 | name: newName, 468 | hasAny, 469 | hasConversion: convertibleTypes.length > 0, 470 | restParam: param.restParam 471 | } 472 | } 473 | 474 | /** 475 | * Return the set of type names in a parameter. 476 | * Caches the result for efficiency 477 | * 478 | * @param {Param} param 479 | * @return {Set} typenames 480 | */ 481 | function paramTypeSet (param) { 482 | if (!param.typeSet) { 483 | param.typeSet = new Set() 484 | param.types.forEach(type => param.typeSet.add(type.name)) 485 | } 486 | return param.typeSet 487 | } 488 | 489 | /** 490 | * Parse a signature with comma separated parameters, 491 | * like "number | boolean, ...string" 492 | * 493 | * @param {string} signature 494 | * @return {Param[]} params 495 | */ 496 | function parseSignature (rawSignature) { 497 | const params = [] 498 | if (typeof rawSignature !== 'string') { 499 | throw new TypeError('Signatures must be strings') 500 | } 501 | const signature = rawSignature.trim() 502 | if (signature === '') { 503 | return params 504 | } 505 | 506 | const rawParams = signature.split(',') 507 | for (let i = 0; i < rawParams.length; ++i) { 508 | const parsedParam = parseParam(rawParams[i].trim()) 509 | if (parsedParam.restParam && (i !== rawParams.length - 1)) { 510 | throw new SyntaxError( 511 | 'Unexpected rest parameter "' + rawParams[i] + '": ' + 512 | 'only allowed for the last parameter') 513 | } 514 | // if invalid, short-circuit (all the types may have been filtered) 515 | if (parsedParam.types.length === 0) { 516 | return null 517 | } 518 | params.push(parsedParam) 519 | } 520 | 521 | return params 522 | } 523 | 524 | /** 525 | * Test whether a set of params contains a restParam 526 | * @param {Param[]} params 527 | * @return {boolean} Returns true when the last parameter is a restParam 528 | */ 529 | function hasRestParam (params) { 530 | const param = last(params) 531 | return param ? param.restParam : false 532 | } 533 | 534 | /** 535 | * Create a type test for a single parameter, which can have one or multiple 536 | * types. 537 | * @param {Param} param 538 | * @return {function(x: *) : boolean} Returns a test function 539 | */ 540 | function compileTest (param) { 541 | if (!param || param.types.length === 0) { 542 | // nothing to do 543 | return ok 544 | } else if (param.types.length === 1) { 545 | return findType(param.types[0].name).test 546 | } else if (param.types.length === 2) { 547 | const test0 = findType(param.types[0].name).test 548 | const test1 = findType(param.types[1].name).test 549 | return function or (x) { 550 | return test0(x) || test1(x) 551 | } 552 | } else { // param.types.length > 2 553 | const tests = param.types.map(function (type) { 554 | return findType(type.name).test 555 | }) 556 | return function or (x) { 557 | for (let i = 0; i < tests.length; i++) { 558 | if (tests[i](x)) { 559 | return true 560 | } 561 | } 562 | return false 563 | } 564 | } 565 | } 566 | 567 | /** 568 | * Create a test for all parameters of a signature 569 | * @param {Param[]} params 570 | * @return {function(args: Array<*>) : boolean} 571 | */ 572 | function compileTests (params) { 573 | let tests, test0, test1 574 | 575 | if (hasRestParam(params)) { 576 | // variable arguments like '...number' 577 | tests = initial(params).map(compileTest) 578 | const varIndex = tests.length 579 | const lastTest = compileTest(last(params)) 580 | const testRestParam = function (args) { 581 | for (let i = varIndex; i < args.length; i++) { 582 | if (!lastTest(args[i])) { 583 | return false 584 | } 585 | } 586 | return true 587 | } 588 | 589 | return function testArgs (args) { 590 | for (let i = 0; i < tests.length; i++) { 591 | if (!tests[i](args[i])) { 592 | return false 593 | } 594 | } 595 | return testRestParam(args) && (args.length >= varIndex + 1) 596 | } 597 | } else { 598 | // no variable arguments 599 | if (params.length === 0) { 600 | return function testArgs (args) { 601 | return args.length === 0 602 | } 603 | } else if (params.length === 1) { 604 | test0 = compileTest(params[0]) 605 | return function testArgs (args) { 606 | return test0(args[0]) && args.length === 1 607 | } 608 | } else if (params.length === 2) { 609 | test0 = compileTest(params[0]) 610 | test1 = compileTest(params[1]) 611 | return function testArgs (args) { 612 | return test0(args[0]) && test1(args[1]) && args.length === 2 613 | } 614 | } else { // arguments.length > 2 615 | tests = params.map(compileTest) 616 | return function testArgs (args) { 617 | for (let i = 0; i < tests.length; i++) { 618 | if (!tests[i](args[i])) { 619 | return false 620 | } 621 | } 622 | return args.length === tests.length 623 | } 624 | } 625 | } 626 | } 627 | 628 | /** 629 | * Find the parameter at a specific index of a Params list. 630 | * Handles rest parameters. 631 | * @param {Param[]} params 632 | * @param {number} index 633 | * @return {Param | null} Returns the matching parameter when found, 634 | * null otherwise. 635 | */ 636 | function getParamAtIndex (params, index) { 637 | return index < params.length 638 | ? params[index] 639 | : hasRestParam(params) ? last(params) : null 640 | } 641 | 642 | /** 643 | * Get all type names of a parameter 644 | * @param {Params[]} params 645 | * @param {number} index 646 | * @return {string[]} Returns an array with type names 647 | */ 648 | function getTypeSetAtIndex (params, index) { 649 | const param = getParamAtIndex(params, index) 650 | if (!param) { 651 | return new Set() 652 | } 653 | return paramTypeSet(param) 654 | } 655 | 656 | /** 657 | * Test whether a type is an exact type or conversion 658 | * @param {Type} type 659 | * @return {boolean} Returns true when 660 | */ 661 | function isExactType (type) { 662 | return type.conversion === null || type.conversion === undefined 663 | } 664 | 665 | /** 666 | * Helper function for creating error messages: create an array with 667 | * all available types on a specific argument index. 668 | * @param {Signature[]} signatures 669 | * @param {number} index 670 | * @return {string[]} Returns an array with available types 671 | */ 672 | function mergeExpectedParams (signatures, index) { 673 | const typeSet = new Set() 674 | signatures.forEach(signature => { 675 | const paramSet = getTypeSetAtIndex(signature.params, index) 676 | let name 677 | for (name of paramSet) { 678 | typeSet.add(name) 679 | } 680 | }) 681 | 682 | return typeSet.has('any') ? ['any'] : Array.from(typeSet) 683 | } 684 | 685 | /** 686 | * Create 687 | * @param {string} name The name of the function 688 | * @param {array.<*>} args The actual arguments passed to the function 689 | * @param {Signature[]} signatures A list with available signatures 690 | * @return {TypeError} Returns a type error with additional data 691 | * attached to it in the property `data` 692 | */ 693 | function createError (name, args, signatures) { 694 | let err, expected 695 | const _name = name || 'unnamed' 696 | 697 | // test for wrong type at some index 698 | let matchingSignatures = signatures 699 | let index 700 | for (index = 0; index < args.length; index++) { 701 | const nextMatchingDefs = [] 702 | matchingSignatures.forEach(signature => { 703 | const param = getParamAtIndex(signature.params, index) 704 | const test = compileTest(param) 705 | if ((index < signature.params.length || 706 | hasRestParam(signature.params)) && 707 | test(args[index])) { 708 | nextMatchingDefs.push(signature) 709 | } 710 | }) 711 | 712 | if (nextMatchingDefs.length === 0) { 713 | // no matching signatures anymore, throw error "wrong type" 714 | expected = mergeExpectedParams(matchingSignatures, index) 715 | if (expected.length > 0) { 716 | const actualTypes = findTypeNames(args[index]) 717 | 718 | err = new TypeError('Unexpected type of argument in function ' + _name + 719 | ' (expected: ' + expected.join(' or ') + 720 | ', actual: ' + actualTypes.join(' | ') + ', index: ' + index + ')') 721 | err.data = { 722 | category: 'wrongType', 723 | fn: _name, 724 | index, 725 | actual: actualTypes, 726 | expected 727 | } 728 | return err 729 | } 730 | } else { 731 | matchingSignatures = nextMatchingDefs 732 | } 733 | } 734 | 735 | // test for too few arguments 736 | const lengths = matchingSignatures.map(function (signature) { 737 | return hasRestParam(signature.params) 738 | ? Infinity 739 | : signature.params.length 740 | }) 741 | if (args.length < Math.min.apply(null, lengths)) { 742 | expected = mergeExpectedParams(matchingSignatures, index) 743 | err = new TypeError('Too few arguments in function ' + _name + 744 | ' (expected: ' + expected.join(' or ') + 745 | ', index: ' + args.length + ')') 746 | err.data = { 747 | category: 'tooFewArgs', 748 | fn: _name, 749 | index: args.length, 750 | expected 751 | } 752 | return err 753 | } 754 | 755 | // test for too many arguments 756 | const maxLength = Math.max.apply(null, lengths) 757 | if (args.length > maxLength) { 758 | err = new TypeError('Too many arguments in function ' + _name + 759 | ' (expected: ' + maxLength + ', actual: ' + args.length + ')') 760 | err.data = { 761 | category: 'tooManyArgs', 762 | fn: _name, 763 | index: args.length, 764 | expectedLength: maxLength 765 | } 766 | return err 767 | } 768 | 769 | // Generic error 770 | const argTypes = [] 771 | for (let i = 0; i < args.length; ++i) { 772 | argTypes.push(findTypeNames(args[i]).join('|')) 773 | } 774 | err = new TypeError('Arguments of type "' + argTypes.join(', ') + 775 | '" do not match any of the defined signatures of function ' + _name + '.') 776 | err.data = { 777 | category: 'mismatch', 778 | actual: argTypes 779 | } 780 | return err 781 | } 782 | 783 | /** 784 | * Find the lowest index of all exact types of a parameter (no conversions) 785 | * @param {Param} param 786 | * @return {number} Returns the index of the lowest type in typed.types 787 | */ 788 | function getLowestTypeIndex (param) { 789 | let min = typeList.length + 1 790 | 791 | for (let i = 0; i < param.types.length; i++) { 792 | if (isExactType(param.types[i])) { 793 | min = Math.min(min, param.types[i].typeIndex) 794 | } 795 | } 796 | 797 | return min 798 | } 799 | 800 | /** 801 | * Find the lowest index of the conversion of all types of the parameter 802 | * having a conversion 803 | * @param {Param} param 804 | * @return {number} Returns the lowest index of the conversions of this type 805 | */ 806 | function getLowestConversionIndex (param) { 807 | let min = nConversions + 1 808 | 809 | for (let i = 0; i < param.types.length; i++) { 810 | if (!isExactType(param.types[i])) { 811 | min = Math.min(min, param.types[i].conversionIndex) 812 | } 813 | } 814 | 815 | return min 816 | } 817 | 818 | /** 819 | * Compare two params 820 | * @param {Param} param1 821 | * @param {Param} param2 822 | * @return {number} returns -1 when param1 must get a lower 823 | * index than param2, 1 when the opposite, 824 | * or zero when both are equal 825 | */ 826 | function compareParams (param1, param2) { 827 | // We compare a number of metrics on a param in turn: 828 | // 1) 'any' parameters are the least preferred 829 | if (param1.hasAny) { 830 | if (!param2.hasAny) { 831 | return 1 832 | } 833 | } else if (param2.hasAny) { 834 | return -1 835 | } 836 | 837 | // 2) Prefer non-rest to rest parameters 838 | if (param1.restParam) { 839 | if (!param2.restParam) { 840 | return 1 841 | } 842 | } else if (param2.restParam) { 843 | return -1 844 | } 845 | 846 | // 3) Prefer exact type match to conversions 847 | if (param1.hasConversion) { 848 | if (!param2.hasConversion) { 849 | return 1 850 | } 851 | } else if (param2.hasConversion) { 852 | return -1 853 | } 854 | 855 | // 4) Prefer lower type index: 856 | const typeDiff = getLowestTypeIndex(param1) - getLowestTypeIndex(param2) 857 | if (typeDiff < 0) { 858 | return -1 859 | } 860 | if (typeDiff > 0) { 861 | return 1 862 | } 863 | 864 | // 5) Prefer lower conversion index 865 | const convDiff = 866 | getLowestConversionIndex(param1) - getLowestConversionIndex(param2) 867 | if (convDiff < 0) { 868 | return -1 869 | } 870 | if (convDiff > 0) { 871 | return 1 872 | } 873 | 874 | // Don't have a basis for preference 875 | return 0 876 | } 877 | 878 | /** 879 | * Compare two signatures 880 | * @param {Signature} signature1 881 | * @param {Signature} signature2 882 | * @return {number} returns a negative number when param1 must get a lower 883 | * index than param2, a positive number when the opposite, 884 | * or zero when both are equal 885 | */ 886 | function compareSignatures (signature1, signature2) { 887 | const pars1 = signature1.params 888 | const pars2 = signature2.params 889 | const last1 = last(pars1) 890 | const last2 = last(pars2) 891 | const hasRest1 = hasRestParam(pars1) 892 | const hasRest2 = hasRestParam(pars2) 893 | // We compare a number of metrics on signatures in turn: 894 | // 1) An "any rest param" is least preferred 895 | if (hasRest1 && last1.hasAny) { 896 | if (!hasRest2 || !last2.hasAny) { 897 | return 1 898 | } 899 | } else if (hasRest2 && last2.hasAny) { 900 | return -1 901 | } 902 | 903 | // 2) Minimize the number of 'any' parameters 904 | let any1 = 0 905 | let conv1 = 0 906 | let par 907 | for (par of pars1) { 908 | if (par.hasAny) ++any1 909 | if (par.hasConversion) ++conv1 910 | } 911 | let any2 = 0 912 | let conv2 = 0 913 | for (par of pars2) { 914 | if (par.hasAny) ++any2 915 | if (par.hasConversion) ++conv2 916 | } 917 | if (any1 !== any2) { 918 | return any1 - any2 919 | } 920 | 921 | // 3) A conversion rest param is less preferred 922 | if (hasRest1 && last1.hasConversion) { 923 | if (!hasRest2 || !last2.hasConversion) { 924 | return 1 925 | } 926 | } else if (hasRest2 && last2.hasConversion) { 927 | return -1 928 | } 929 | 930 | // 4) Minimize the number of conversions 931 | if (conv1 !== conv2) { 932 | return conv1 - conv2 933 | } 934 | 935 | // 5) Prefer no rest param 936 | if (hasRest1) { 937 | if (!hasRest2) { 938 | return 1 939 | } 940 | } else if (hasRest2) { 941 | return -1 942 | } 943 | 944 | // 6) Prefer shorter with rest param, longer without 945 | const lengthCriterion = 946 | (pars1.length - pars2.length) * (hasRest1 ? -1 : 1) 947 | if (lengthCriterion !== 0) { 948 | return lengthCriterion 949 | } 950 | 951 | // Signatures are identical in each of the above metrics. 952 | // In particular, they are the same length. 953 | // We can therefore compare the parameters one by one. 954 | // First we count which signature has more preferred parameters. 955 | const comparisons = [] 956 | let tc = 0 957 | for (let i = 0; i < pars1.length; ++i) { 958 | const thisComparison = compareParams(pars1[i], pars2[i]) 959 | comparisons.push(thisComparison) 960 | tc += thisComparison 961 | } 962 | if (tc !== 0) { 963 | return tc 964 | } 965 | 966 | // They have the same number of preferred parameters, so go by the 967 | // earliest parameter in which we have a preference. 968 | // In other words, dispatch is driven somewhat more by earlier 969 | // parameters than later ones. 970 | let c 971 | for (c of comparisons) { 972 | if (c !== 0) { 973 | return c 974 | } 975 | } 976 | 977 | // It's a tossup: 978 | return 0 979 | } 980 | 981 | /** 982 | * Produce a list of all conversions from distinct types to one of 983 | * the given types. 984 | * 985 | * @param {string[]} typeNames 986 | * @return {ConversionDef[]} Returns the conversions that are available 987 | * resulting in any given type (if any) 988 | */ 989 | function availableConversions (typeNames) { 990 | if (typeNames.length === 0) { 991 | return [] 992 | } 993 | const types = typeNames.map(findType) 994 | if (typeNames.length > 1) { 995 | types.sort((t1, t2) => t1.index - t2.index) 996 | } 997 | let matches = types[0].conversionsTo 998 | if (typeNames.length === 1) { 999 | return matches 1000 | } 1001 | 1002 | matches = matches.concat([]) // shallow copy the matches 1003 | // Since the types are now in index order, we just want the first 1004 | // occurrence of any from type: 1005 | const knownTypes = new Set(typeNames) 1006 | for (let i = 1; i < types.length; ++i) { 1007 | let newMatch 1008 | for (newMatch of types[i].conversionsTo) { 1009 | if (!knownTypes.has(newMatch.from)) { 1010 | matches.push(newMatch) 1011 | knownTypes.add(newMatch.from) 1012 | } 1013 | } 1014 | } 1015 | 1016 | return matches 1017 | } 1018 | 1019 | /** 1020 | * Preprocess arguments before calling the original function: 1021 | * - if needed convert the parameters 1022 | * - in case of rest parameters, move the rest parameters into an Array 1023 | * @param {Param[]} params 1024 | * @param {function} fn 1025 | * @return {function} Returns a wrapped function 1026 | */ 1027 | function compileArgsPreprocessing (params, fn) { 1028 | let fnConvert = fn 1029 | 1030 | // TODO: can we make this wrapper function smarter/simpler? 1031 | 1032 | if (params.some(p => p.hasConversion)) { 1033 | const restParam = hasRestParam(params) 1034 | const compiledConversions = params.map(compileArgConversion) 1035 | 1036 | fnConvert = function convertArgs () { 1037 | const args = [] 1038 | const last = restParam ? arguments.length - 1 : arguments.length 1039 | for (let i = 0; i < last; i++) { 1040 | args[i] = compiledConversions[i](arguments[i]) 1041 | } 1042 | if (restParam) { 1043 | args[last] = arguments[last].map(compiledConversions[last]) 1044 | } 1045 | 1046 | return fn.apply(this, args) 1047 | } 1048 | } 1049 | 1050 | let fnPreprocess = fnConvert 1051 | if (hasRestParam(params)) { 1052 | const offset = params.length - 1 1053 | 1054 | fnPreprocess = function preprocessRestParams () { 1055 | return fnConvert.apply(this, 1056 | slice(arguments, 0, offset).concat([slice(arguments, offset)])) 1057 | } 1058 | } 1059 | 1060 | return fnPreprocess 1061 | } 1062 | 1063 | /** 1064 | * Compile conversion for a parameter to the right type 1065 | * @param {Param} param 1066 | * @return {function} Returns the wrapped function that will convert arguments 1067 | * 1068 | */ 1069 | function compileArgConversion (param) { 1070 | let test0, test1, conversion0, conversion1 1071 | const tests = [] 1072 | const conversions = [] 1073 | 1074 | param.types.forEach(function (type) { 1075 | if (type.conversion) { 1076 | tests.push(findType(type.conversion.from).test) 1077 | conversions.push(type.conversion.convert) 1078 | } 1079 | }) 1080 | 1081 | // create optimized conversion functions depending on the number of conversions 1082 | switch (conversions.length) { 1083 | case 0: 1084 | return function convertArg (arg) { 1085 | return arg 1086 | } 1087 | 1088 | case 1: 1089 | test0 = tests[0] 1090 | conversion0 = conversions[0] 1091 | return function convertArg (arg) { 1092 | if (test0(arg)) { 1093 | return conversion0(arg) 1094 | } 1095 | return arg 1096 | } 1097 | 1098 | case 2: 1099 | test0 = tests[0] 1100 | test1 = tests[1] 1101 | conversion0 = conversions[0] 1102 | conversion1 = conversions[1] 1103 | return function convertArg (arg) { 1104 | if (test0(arg)) { 1105 | return conversion0(arg) 1106 | } 1107 | if (test1(arg)) { 1108 | return conversion1(arg) 1109 | } 1110 | return arg 1111 | } 1112 | 1113 | default: 1114 | return function convertArg (arg) { 1115 | for (let i = 0; i < conversions.length; i++) { 1116 | if (tests[i](arg)) { 1117 | return conversions[i](arg) 1118 | } 1119 | } 1120 | return arg 1121 | } 1122 | } 1123 | } 1124 | 1125 | /** 1126 | * Split params with union types in to separate params. 1127 | * 1128 | * For example: 1129 | * 1130 | * splitParams([['Array', 'Object'], ['string', 'RegExp']) 1131 | * // returns: 1132 | * // [ 1133 | * // ['Array', 'string'], 1134 | * // ['Array', 'RegExp'], 1135 | * // ['Object', 'string'], 1136 | * // ['Object', 'RegExp'] 1137 | * // ] 1138 | * 1139 | * @param {Param[]} params 1140 | * @return {Param[]} 1141 | */ 1142 | function splitParams (params) { 1143 | function _splitParams (params, index, paramsSoFar) { 1144 | if (index < params.length) { 1145 | const param = params[index] 1146 | let resultingParams = [] 1147 | 1148 | if (param.restParam) { 1149 | // split the types of a rest parameter in two: 1150 | // one with only exact types, and one with exact types and conversions 1151 | const exactTypes = param.types.filter(isExactType) 1152 | if (exactTypes.length < param.types.length) { 1153 | resultingParams.push({ 1154 | types: exactTypes, 1155 | name: '...' + exactTypes.map(t => t.name).join('|'), 1156 | hasAny: exactTypes.some(t => t.isAny), 1157 | hasConversion: false, 1158 | restParam: true 1159 | }) 1160 | } 1161 | resultingParams.push(param) 1162 | } else { 1163 | // split all the types of a regular parameter into one type per param 1164 | resultingParams = param.types.map(function (type) { 1165 | return { 1166 | types: [type], 1167 | name: type.name, 1168 | hasAny: type.isAny, 1169 | hasConversion: type.conversion, 1170 | restParam: false 1171 | } 1172 | }) 1173 | } 1174 | 1175 | // recurse over the groups with types 1176 | return flatMap(resultingParams, function (nextParam) { 1177 | return _splitParams(params, index + 1, paramsSoFar.concat([nextParam])) 1178 | }) 1179 | } else { 1180 | // we've reached the end of the parameters. 1181 | return [paramsSoFar] 1182 | } 1183 | } 1184 | 1185 | return _splitParams(params, 0, []) 1186 | } 1187 | 1188 | /** 1189 | * Test whether two param lists represent conflicting signatures 1190 | * @param {Param[]} params1 1191 | * @param {Param[]} params2 1192 | * @return {boolean} Returns true when the signatures conflict, false otherwise. 1193 | */ 1194 | function conflicting (params1, params2) { 1195 | const ii = Math.max(params1.length, params2.length) 1196 | 1197 | for (let i = 0; i < ii; i++) { 1198 | const typeSet1 = getTypeSetAtIndex(params1, i) 1199 | const typeSet2 = getTypeSetAtIndex(params2, i) 1200 | let overlap = false 1201 | let name 1202 | for (name of typeSet2) { 1203 | if (typeSet1.has(name)) { 1204 | overlap = true 1205 | break 1206 | } 1207 | } 1208 | if (!overlap) { 1209 | return false 1210 | } 1211 | } 1212 | 1213 | const len1 = params1.length 1214 | const len2 = params2.length 1215 | const restParam1 = hasRestParam(params1) 1216 | const restParam2 = hasRestParam(params2) 1217 | 1218 | return restParam1 1219 | ? restParam2 ? (len1 === len2) : (len2 >= len1) 1220 | : restParam2 ? (len1 >= len2) : (len1 === len2) 1221 | } 1222 | 1223 | /** 1224 | * Helper function for `resolveReferences` that returns a copy of 1225 | * functionList wihe any prior resolutions cleared out, in case we are 1226 | * recycling signatures from a prior typed function construction. 1227 | * 1228 | * @param {Array.} functionList 1229 | * @return {Array.} 1230 | */ 1231 | function clearResolutions (functionList) { 1232 | return functionList.map(fn => { 1233 | if (isReferToSelf(fn)) { 1234 | return referToSelf(fn.referToSelf.callback) 1235 | } 1236 | if (isReferTo(fn)) { 1237 | return makeReferTo(fn.referTo.references, fn.referTo.callback) 1238 | } 1239 | return fn 1240 | }) 1241 | } 1242 | 1243 | /** 1244 | * Take a list of references, a list of functions functionList, and a 1245 | * signatureMap indexing signatures into functionList, and return 1246 | * the list of resolutions, or a false-y value if they don't all 1247 | * resolve in a valid way (yet). 1248 | * 1249 | * @param {string[]} references 1250 | * @param {Array} signatureMap 1252 | * @return {function[] | false} resolutions 1253 | */ 1254 | function collectResolutions (references, functionList, signatureMap) { 1255 | const resolvedReferences = [] 1256 | let reference 1257 | for (reference of references) { 1258 | let resolution = signatureMap[reference] 1259 | if (typeof resolution !== 'number') { 1260 | throw new TypeError( 1261 | 'No definition for referenced signature "' + reference + '"') 1262 | } 1263 | resolution = functionList[resolution] 1264 | if (typeof resolution !== 'function') { 1265 | return false 1266 | } 1267 | resolvedReferences.push(resolution) 1268 | } 1269 | return resolvedReferences 1270 | } 1271 | 1272 | /** 1273 | * Resolve any references in the functionList for the typed function 1274 | * itself. The signatureMap tells which index in the functionList a 1275 | * given signature should be mapped to (for use in resolving typed.referTo) 1276 | * and self provides the destions of a typed.referToSelf. 1277 | * 1278 | * @param {Array} functionList 1279 | * @param {Object.} signatureMap 1280 | * @param {function} self The typed-function itself 1281 | * @return {Array} The list of resolved functions 1282 | */ 1283 | function resolveReferences (functionList, signatureMap, self) { 1284 | const resolvedFunctions = clearResolutions(functionList) 1285 | const isResolved = new Array(resolvedFunctions.length).fill(false) 1286 | let leftUnresolved = true 1287 | while (leftUnresolved) { 1288 | leftUnresolved = false 1289 | let nothingResolved = true 1290 | for (let i = 0; i < resolvedFunctions.length; ++i) { 1291 | if (isResolved[i]) continue 1292 | const fn = resolvedFunctions[i] 1293 | 1294 | if (isReferToSelf(fn)) { 1295 | resolvedFunctions[i] = fn.referToSelf.callback(self) 1296 | // Preserve reference in case signature is reused someday: 1297 | resolvedFunctions[i].referToSelf = fn.referToSelf 1298 | isResolved[i] = true 1299 | nothingResolved = false 1300 | } else if (isReferTo(fn)) { 1301 | const resolvedReferences = collectResolutions( 1302 | fn.referTo.references, resolvedFunctions, signatureMap) 1303 | if (resolvedReferences) { 1304 | resolvedFunctions[i] = 1305 | fn.referTo.callback.apply(this, resolvedReferences) 1306 | // Preserve reference in case signature is reused someday: 1307 | resolvedFunctions[i].referTo = fn.referTo 1308 | isResolved[i] = true 1309 | nothingResolved = false 1310 | } else { 1311 | leftUnresolved = true 1312 | } 1313 | } 1314 | } 1315 | 1316 | if (nothingResolved && leftUnresolved) { 1317 | throw new SyntaxError( 1318 | 'Circular reference detected in resolving typed.referTo') 1319 | } 1320 | } 1321 | 1322 | return resolvedFunctions 1323 | } 1324 | 1325 | /** 1326 | * Validate whether any of the function bodies contains a self-reference 1327 | * usage like `this(...)` or `this.signatures`. This self-referencing is 1328 | * deprecated since typed-function v3. It has been replaced with 1329 | * the functions typed.referTo and typed.referToSelf. 1330 | * @param {Object.} signaturesMap 1331 | */ 1332 | function validateDeprecatedThis (signaturesMap) { 1333 | // TODO: remove this deprecation warning logic some day (it's introduced in v3) 1334 | 1335 | // match occurrences like 'this(' and 'this.signatures' 1336 | const deprecatedThisRegex = /\bthis(\(|\.signatures\b)/ 1337 | 1338 | Object.keys(signaturesMap).forEach(signature => { 1339 | const fn = signaturesMap[signature] 1340 | 1341 | if (deprecatedThisRegex.test(fn.toString())) { 1342 | throw new SyntaxError('Using `this` to self-reference a function ' + 1343 | 'is deprecated since typed-function@3. ' + 1344 | 'Use typed.referTo and typed.referToSelf instead.') 1345 | } 1346 | }) 1347 | } 1348 | 1349 | /** 1350 | * Create a typed function 1351 | * @param {String} name The name for the typed function 1352 | * @param {Object.} rawSignaturesMap 1353 | * An object with one or 1354 | * multiple signatures as key, and the 1355 | * function corresponding to the 1356 | * signature as value. 1357 | * @return {function} Returns the created typed function. 1358 | */ 1359 | function createTypedFunction (name, rawSignaturesMap) { 1360 | typed.createCount++ 1361 | 1362 | if (Object.keys(rawSignaturesMap).length === 0) { 1363 | throw new SyntaxError('No signatures provided') 1364 | } 1365 | 1366 | if (typed.warnAgainstDeprecatedThis) { 1367 | validateDeprecatedThis(rawSignaturesMap) 1368 | } 1369 | 1370 | // Main processing loop for signatures 1371 | const parsedParams = [] 1372 | const originalFunctions = [] 1373 | const signaturesMap = {} 1374 | const preliminarySignatures = [] // may have duplicates from conversions 1375 | let signature 1376 | for (signature in rawSignaturesMap) { 1377 | // A) Protect against polluted Object prototype: 1378 | if (!Object.prototype.hasOwnProperty.call(rawSignaturesMap, signature)) { 1379 | continue 1380 | } 1381 | // B) Parse the signature 1382 | const params = parseSignature(signature) 1383 | if (!params) continue 1384 | // C) Check for conflicts 1385 | parsedParams.forEach(function (pp) { 1386 | if (conflicting(pp, params)) { 1387 | throw new TypeError('Conflicting signatures "' + 1388 | stringifyParams(pp) + '" and "' + 1389 | stringifyParams(params) + '".') 1390 | } 1391 | }) 1392 | parsedParams.push(params) 1393 | // D) Store the provided function and add conversions 1394 | const functionIndex = originalFunctions.length 1395 | originalFunctions.push(rawSignaturesMap[signature]) 1396 | const conversionParams = params.map(expandParam) 1397 | // E) Split the signatures and collect them up 1398 | let sp 1399 | for (sp of splitParams(conversionParams)) { 1400 | const spName = stringifyParams(sp) 1401 | preliminarySignatures.push( 1402 | { params: sp, name: spName, fn: functionIndex }) 1403 | if (sp.every(p => !p.hasConversion)) { 1404 | signaturesMap[spName] = functionIndex 1405 | } 1406 | } 1407 | } 1408 | 1409 | preliminarySignatures.sort(compareSignatures) 1410 | 1411 | // Note the forward reference to theTypedFn 1412 | const resolvedFunctions = 1413 | resolveReferences(originalFunctions, signaturesMap, theTypedFn) 1414 | 1415 | // Fill in the proper function for each signature 1416 | let s 1417 | for (s in signaturesMap) { 1418 | if (Object.prototype.hasOwnProperty.call(signaturesMap, s)) { 1419 | signaturesMap[s] = resolvedFunctions[signaturesMap[s]] 1420 | } 1421 | } 1422 | const signatures = [] 1423 | const internalSignatureMap = new Map() // benchmarks faster than object 1424 | for (s of preliminarySignatures) { 1425 | // Note it's only safe to eliminate duplicates like this 1426 | // _after_ the signature sorting step above; otherwise we might 1427 | // remove the wrong one. 1428 | if (!internalSignatureMap.has(s.name)) { 1429 | s.fn = resolvedFunctions[s.fn] 1430 | signatures.push(s) 1431 | internalSignatureMap.set(s.name, s) 1432 | } 1433 | } 1434 | 1435 | // we create a highly optimized checks for the first couple of signatures with max 2 arguments 1436 | const ok0 = signatures[0] && signatures[0].params.length <= 2 && !hasRestParam(signatures[0].params) 1437 | const ok1 = signatures[1] && signatures[1].params.length <= 2 && !hasRestParam(signatures[1].params) 1438 | const ok2 = signatures[2] && signatures[2].params.length <= 2 && !hasRestParam(signatures[2].params) 1439 | const ok3 = signatures[3] && signatures[3].params.length <= 2 && !hasRestParam(signatures[3].params) 1440 | const ok4 = signatures[4] && signatures[4].params.length <= 2 && !hasRestParam(signatures[4].params) 1441 | const ok5 = signatures[5] && signatures[5].params.length <= 2 && !hasRestParam(signatures[5].params) 1442 | const allOk = ok0 && ok1 && ok2 && ok3 && ok4 && ok5 1443 | 1444 | // compile the tests 1445 | for (let i = 0; i < signatures.length; ++i) { 1446 | signatures[i].test = compileTests(signatures[i].params) 1447 | } 1448 | 1449 | const test00 = ok0 ? compileTest(signatures[0].params[0]) : notOk 1450 | const test10 = ok1 ? compileTest(signatures[1].params[0]) : notOk 1451 | const test20 = ok2 ? compileTest(signatures[2].params[0]) : notOk 1452 | const test30 = ok3 ? compileTest(signatures[3].params[0]) : notOk 1453 | const test40 = ok4 ? compileTest(signatures[4].params[0]) : notOk 1454 | const test50 = ok5 ? compileTest(signatures[5].params[0]) : notOk 1455 | 1456 | const test01 = ok0 ? compileTest(signatures[0].params[1]) : notOk 1457 | const test11 = ok1 ? compileTest(signatures[1].params[1]) : notOk 1458 | const test21 = ok2 ? compileTest(signatures[2].params[1]) : notOk 1459 | const test31 = ok3 ? compileTest(signatures[3].params[1]) : notOk 1460 | const test41 = ok4 ? compileTest(signatures[4].params[1]) : notOk 1461 | const test51 = ok5 ? compileTest(signatures[5].params[1]) : notOk 1462 | 1463 | // compile the functions 1464 | for (let i = 0; i < signatures.length; ++i) { 1465 | signatures[i].implementation = 1466 | compileArgsPreprocessing(signatures[i].params, signatures[i].fn) 1467 | } 1468 | 1469 | const fn0 = ok0 ? signatures[0].implementation : undef 1470 | const fn1 = ok1 ? signatures[1].implementation : undef 1471 | const fn2 = ok2 ? signatures[2].implementation : undef 1472 | const fn3 = ok3 ? signatures[3].implementation : undef 1473 | const fn4 = ok4 ? signatures[4].implementation : undef 1474 | const fn5 = ok5 ? signatures[5].implementation : undef 1475 | 1476 | const len0 = ok0 ? signatures[0].params.length : -1 1477 | const len1 = ok1 ? signatures[1].params.length : -1 1478 | const len2 = ok2 ? signatures[2].params.length : -1 1479 | const len3 = ok3 ? signatures[3].params.length : -1 1480 | const len4 = ok4 ? signatures[4].params.length : -1 1481 | const len5 = ok5 ? signatures[5].params.length : -1 1482 | 1483 | // simple and generic, but also slow 1484 | const iStart = allOk ? 6 : 0 1485 | const iEnd = signatures.length 1486 | // de-reference ahead for execution speed: 1487 | const tests = signatures.map(s => s.test) 1488 | const fns = signatures.map(s => s.implementation) 1489 | const generic = function generic () { 1490 | 'use strict' 1491 | 1492 | for (let i = iStart; i < iEnd; i++) { 1493 | if (tests[i](arguments)) { 1494 | return fns[i].apply(this, arguments) 1495 | } 1496 | } 1497 | 1498 | return typed.onMismatch(name, arguments, signatures) 1499 | } 1500 | 1501 | // create the typed function 1502 | // fast, specialized version. Falls back to the slower, generic one if needed 1503 | function theTypedFn (arg0, arg1) { 1504 | 'use strict' 1505 | 1506 | if (arguments.length === len0 && test00(arg0) && test01(arg1)) { return fn0.apply(this, arguments) } 1507 | if (arguments.length === len1 && test10(arg0) && test11(arg1)) { return fn1.apply(this, arguments) } 1508 | if (arguments.length === len2 && test20(arg0) && test21(arg1)) { return fn2.apply(this, arguments) } 1509 | if (arguments.length === len3 && test30(arg0) && test31(arg1)) { return fn3.apply(this, arguments) } 1510 | if (arguments.length === len4 && test40(arg0) && test41(arg1)) { return fn4.apply(this, arguments) } 1511 | if (arguments.length === len5 && test50(arg0) && test51(arg1)) { return fn5.apply(this, arguments) } 1512 | 1513 | return generic.apply(this, arguments) 1514 | } 1515 | 1516 | // attach name the typed function 1517 | try { 1518 | Object.defineProperty(theTypedFn, 'name', { value: name }) 1519 | } catch (err) { 1520 | // old browsers do not support Object.defineProperty and some don't support setting the name property 1521 | // the function name is not essential for the functioning, it's mostly useful for debugging, 1522 | // so it's fine to have unnamed functions. 1523 | } 1524 | 1525 | // attach signatures to the function. 1526 | // This property is close to the original collection of signatures 1527 | // used to create the typed-function, just with unions split: 1528 | theTypedFn.signatures = signaturesMap 1529 | 1530 | // Store internal data for functions like resolve, find, etc. 1531 | // Also serves as the flag that this is a typed-function 1532 | theTypedFn._typedFunctionData = { 1533 | signatures, 1534 | signatureMap: internalSignatureMap 1535 | } 1536 | 1537 | return theTypedFn 1538 | } 1539 | 1540 | /** 1541 | * Action to take on mismatch 1542 | * @param {string} name Name of function that was attempted to be called 1543 | * @param {Array} args Actual arguments to the call 1544 | * @param {Array} signatures Known signatures of the named typed-function 1545 | */ 1546 | function _onMismatch (name, args, signatures) { 1547 | throw createError(name, args, signatures) 1548 | } 1549 | 1550 | /** 1551 | * Return all but the last items of an array or function Arguments 1552 | * @param {Array | Arguments} arr 1553 | * @return {Array} 1554 | */ 1555 | function initial (arr) { 1556 | return slice(arr, 0, arr.length - 1) 1557 | } 1558 | 1559 | /** 1560 | * return the last item of an array or function Arguments 1561 | * @param {Array | Arguments} arr 1562 | * @return {*} 1563 | */ 1564 | function last (arr) { 1565 | return arr[arr.length - 1] 1566 | } 1567 | 1568 | /** 1569 | * Slice an array or function Arguments 1570 | * @param {Array | Arguments | IArguments} arr 1571 | * @param {number} start 1572 | * @param {number} [end] 1573 | * @return {Array} 1574 | */ 1575 | function slice (arr, start, end) { 1576 | return Array.prototype.slice.call(arr, start, end) 1577 | } 1578 | 1579 | /** 1580 | * Return the first item from an array for which test(arr[i]) returns true 1581 | * @param {Array} arr 1582 | * @param {function} test 1583 | * @return {* | undefined} Returns the first matching item 1584 | * or undefined when there is no match 1585 | */ 1586 | function findInArray (arr, test) { 1587 | for (let i = 0; i < arr.length; i++) { 1588 | if (test(arr[i])) { 1589 | return arr[i] 1590 | } 1591 | } 1592 | return undefined 1593 | } 1594 | 1595 | /** 1596 | * Flat map the result invoking a callback for every item in an array. 1597 | * https://gist.github.com/samgiles/762ee337dff48623e729 1598 | * @param {Array} arr 1599 | * @param {function} callback 1600 | * @return {Array} 1601 | */ 1602 | function flatMap (arr, callback) { 1603 | return Array.prototype.concat.apply([], arr.map(callback)) 1604 | } 1605 | 1606 | /** 1607 | * Create a reference callback to one or multiple signatures 1608 | * 1609 | * Syntax: 1610 | * 1611 | * typed.referTo(signature1, signature2, ..., function callback(fn1, fn2, ...) { 1612 | * // ... 1613 | * }) 1614 | * 1615 | * @returns {{referTo: {references: string[], callback}}} 1616 | */ 1617 | function referTo () { 1618 | const references = 1619 | initial(arguments).map(s => stringifyParams(parseSignature(s))) 1620 | const callback = last(arguments) 1621 | 1622 | if (typeof callback !== 'function') { 1623 | throw new TypeError('Callback function expected as last argument') 1624 | } 1625 | 1626 | return makeReferTo(references, callback) 1627 | } 1628 | 1629 | function makeReferTo (references, callback) { 1630 | return { referTo: { references, callback } } 1631 | } 1632 | 1633 | /** 1634 | * Create a reference callback to the typed-function itself 1635 | * 1636 | * @param {(self: function) => function} callback 1637 | * @returns {{referToSelf: { callback: function }}} 1638 | */ 1639 | function referToSelf (callback) { 1640 | if (typeof callback !== 'function') { 1641 | throw new TypeError('Callback function expected as first argument') 1642 | } 1643 | 1644 | return { referToSelf: { callback } } 1645 | } 1646 | 1647 | /** 1648 | * Test whether something is a referTo object, holding a list with reference 1649 | * signatures and a callback. 1650 | * 1651 | * @param {Object | function} objectOrFn 1652 | * @returns {boolean} 1653 | */ 1654 | function isReferTo (objectOrFn) { 1655 | return objectOrFn && 1656 | typeof objectOrFn.referTo === 'object' && 1657 | Array.isArray(objectOrFn.referTo.references) && 1658 | typeof objectOrFn.referTo.callback === 'function' 1659 | } 1660 | 1661 | /** 1662 | * Test whether something is a referToSelf object, holding a callback where 1663 | * to pass `self`. 1664 | * 1665 | * @param {Object | function} objectOrFn 1666 | * @returns {boolean} 1667 | */ 1668 | function isReferToSelf (objectOrFn) { 1669 | return objectOrFn && 1670 | typeof objectOrFn.referToSelf === 'object' && 1671 | typeof objectOrFn.referToSelf.callback === 'function' 1672 | } 1673 | 1674 | /** 1675 | * Check if name is (A) new, (B) a match, or (C) a mismatch; and throw 1676 | * an error in case (C). 1677 | * 1678 | * @param { string | undefined } nameSoFar 1679 | * @param { string | undefined } newName 1680 | * @returns { string } updated name 1681 | */ 1682 | function checkName (nameSoFar, newName) { 1683 | if (!nameSoFar) { 1684 | return newName 1685 | } 1686 | if (newName && newName !== nameSoFar) { 1687 | const err = new Error('Function names do not match (expected: ' + 1688 | nameSoFar + ', actual: ' + newName + ')') 1689 | err.data = { actual: newName, expected: nameSoFar } 1690 | throw err 1691 | } 1692 | return nameSoFar 1693 | } 1694 | 1695 | /** 1696 | * Retrieve the implied name from an object with signature keys 1697 | * and function values, checking whether all value names match 1698 | * 1699 | * @param { {string: function} } obj 1700 | */ 1701 | function getObjectName (obj) { 1702 | let name 1703 | for (const key in obj) { 1704 | // Only pay attention to own properties, and only if their values 1705 | // are typed functions or functions with a signature property 1706 | if (Object.prototype.hasOwnProperty.call(obj, key) && 1707 | (isTypedFunction(obj[key]) || 1708 | typeof obj[key].signature === 'string')) { 1709 | name = checkName(name, obj[key].name) 1710 | } 1711 | } 1712 | return name 1713 | } 1714 | 1715 | /** 1716 | * Copy all of the signatures from the second argument into the first, 1717 | * which is modified by side effect, checking for conflicts 1718 | * 1719 | * @param {Object.} dest 1720 | * @param {Object.} source 1721 | */ 1722 | function mergeSignatures (dest, source) { 1723 | let key 1724 | for (key in source) { 1725 | if (Object.prototype.hasOwnProperty.call(source, key)) { 1726 | if (key in dest) { 1727 | if (source[key] !== dest[key]) { 1728 | const err = new Error('Signature "' + key + '" is defined twice') 1729 | err.data = { 1730 | signature: key, 1731 | sourceFunction: source[key], 1732 | destFunction: dest[key] 1733 | } 1734 | throw err 1735 | } 1736 | // else: both signatures point to the same function, that's fine 1737 | } 1738 | dest[key] = source[key] 1739 | } 1740 | } 1741 | } 1742 | 1743 | const saveTyped = typed 1744 | 1745 | /** 1746 | * Originally the main function was a typed function itself, but then 1747 | * it might not be able to generate error messages if the client 1748 | * replaced the type system with different names. 1749 | * 1750 | * Main entry: typed([name], functions/objects with signatures...) 1751 | * 1752 | * Assembles and returns a new typed-function from the given items 1753 | * that provide signatures and implementations, each of which may be 1754 | * * a plain object mapping (string) signatures to implementing functions, 1755 | * * a previously constructed typed function, or 1756 | * * any other single function with a string-valued property `signature`. 1757 | 1758 | * The name of the resulting typed-function will be given by the 1759 | * string-valued name argument if present, or if not, by the name 1760 | * of any of the arguments that have one, as long as any that do are 1761 | * consistent with each other. If no name is specified, the name will be 1762 | * an empty string. 1763 | * 1764 | * @param {string} maybeName [optional] 1765 | * @param {(function|object)[]} signature providers 1766 | * @returns {typed-function} 1767 | */ 1768 | typed = function (maybeName) { 1769 | const named = typeof maybeName === 'string' 1770 | const start = named ? 1 : 0 1771 | let name = named ? maybeName : '' 1772 | const allSignatures = {} 1773 | for (let i = start; i < arguments.length; ++i) { 1774 | const item = arguments[i] 1775 | let theseSignatures = {} 1776 | let thisName 1777 | if (typeof item === 'function') { 1778 | thisName = item.name 1779 | if (typeof item.signature === 'string') { 1780 | // Case 1: Ordinary function with a string 'signature' property 1781 | theseSignatures[item.signature] = item 1782 | } else if (isTypedFunction(item)) { 1783 | // Case 2: Existing typed function 1784 | theseSignatures = item.signatures 1785 | } 1786 | } else if (isPlainObject(item)) { 1787 | // Case 3: Plain object, assume keys = signatures, values = functions 1788 | theseSignatures = item 1789 | if (!named) { 1790 | thisName = getObjectName(item) 1791 | } 1792 | } 1793 | 1794 | if (Object.keys(theseSignatures).length === 0) { 1795 | const err = new TypeError( 1796 | 'Argument to \'typed\' at index ' + i + ' is not a (typed) function, ' + 1797 | 'nor an object with signatures as keys and functions as values.') 1798 | err.data = { index: i, argument: item } 1799 | throw err 1800 | } 1801 | 1802 | if (!named) { 1803 | name = checkName(name, thisName) 1804 | } 1805 | mergeSignatures(allSignatures, theseSignatures) 1806 | } 1807 | 1808 | return createTypedFunction(name || '', allSignatures) 1809 | } 1810 | 1811 | typed.create = create 1812 | typed.createCount = saveTyped.createCount 1813 | typed.onMismatch = _onMismatch 1814 | typed.throwMismatchError = _onMismatch 1815 | typed.createError = createError 1816 | typed.clear = clear 1817 | typed.clearConversions = clearConversions 1818 | typed.addTypes = addTypes 1819 | typed._findType = findType // For unit testing only 1820 | typed.referTo = referTo 1821 | typed.referToSelf = referToSelf 1822 | typed.convert = convert 1823 | typed.findSignature = findSignature 1824 | typed.find = find 1825 | typed.isTypedFunction = isTypedFunction 1826 | typed.warnAgainstDeprecatedThis = true 1827 | 1828 | /** 1829 | * add a type (convenience wrapper for typed.addTypes) 1830 | * @param {{name: string, test: function}} type 1831 | * @param {boolean} [beforeObjectTest=true] 1832 | * If true, the new test will be inserted before 1833 | * the test with name 'Object' (if any), since 1834 | * tests for Object match Array and classes too. 1835 | */ 1836 | typed.addType = function (type, beforeObjectTest) { 1837 | let before = 'any' 1838 | if (beforeObjectTest !== false && typeMap.has('Object')) { 1839 | before = 'Object' 1840 | } 1841 | typed.addTypes([type], before) 1842 | } 1843 | 1844 | /** 1845 | * Verify that the ConversionDef conversion has a valid format. 1846 | * 1847 | * @param {conversionDef} conversion 1848 | * @return {void} 1849 | * @throws {TypeError|SyntaxError} 1850 | */ 1851 | function _validateConversion (conversion) { 1852 | if (!conversion || 1853 | typeof conversion.from !== 'string' || 1854 | typeof conversion.to !== 'string' || 1855 | typeof conversion.convert !== 'function') { 1856 | throw new TypeError('Object with properties {from: string, to: string, convert: function} expected') 1857 | } 1858 | if (conversion.to === conversion.from) { 1859 | throw new SyntaxError( 1860 | 'Illegal to define conversion from "' + conversion.from + 1861 | '" to itself.') 1862 | } 1863 | } 1864 | 1865 | /** 1866 | * Add a conversion 1867 | * 1868 | * @param {ConversionDef} conversion 1869 | * @param {{override: boolean}} [options] 1870 | * @returns {void} 1871 | * @throws {TypeError} 1872 | */ 1873 | typed.addConversion = function (conversion, options = { override: false }) { 1874 | _validateConversion(conversion) 1875 | 1876 | const to = findType(conversion.to) 1877 | const existing = to.conversionsTo.find((other) => other.from === conversion.from) 1878 | 1879 | if (existing) { 1880 | if (options && options.override) { 1881 | typed.removeConversion({ from: existing.from, to: conversion.to, convert: existing.convert }) 1882 | } else { 1883 | throw new Error( 1884 | 'There is already a conversion from "' + conversion.from + '" to "' + 1885 | to.name + '"') 1886 | } 1887 | } 1888 | 1889 | to.conversionsTo.push({ 1890 | from: conversion.from, 1891 | convert: conversion.convert, 1892 | index: nConversions++ 1893 | }) 1894 | } 1895 | 1896 | /** 1897 | * Convenience wrapper to call addConversion on each conversion in a list. 1898 | * 1899 | * @param {ConversionDef[]} conversions 1900 | * @param {{override: boolean}} [options] 1901 | * @returns {void} 1902 | * @throws {TypeError} 1903 | */ 1904 | typed.addConversions = function (conversions, options) { 1905 | conversions.forEach(conversion => typed.addConversion(conversion, options)) 1906 | } 1907 | 1908 | /** 1909 | * Remove the specified conversion. The format is the same as for 1910 | * addConversion, and the convert function must match or an error 1911 | * is thrown. 1912 | * 1913 | * @param {{from: string, to: string, convert: function}} conversion 1914 | * @returns {void} 1915 | * @throws {TypeError|SyntaxError|Error} 1916 | */ 1917 | typed.removeConversion = function (conversion) { 1918 | _validateConversion(conversion) 1919 | const to = findType(conversion.to) 1920 | const existingConversion = 1921 | findInArray(to.conversionsTo, c => (c.from === conversion.from)) 1922 | if (!existingConversion) { 1923 | throw new Error( 1924 | 'Attempt to remove nonexistent conversion from ' + conversion.from + 1925 | ' to ' + conversion.to) 1926 | } 1927 | if (existingConversion.convert !== conversion.convert) { 1928 | throw new Error( 1929 | 'Conversion to remove does not match existing conversion') 1930 | } 1931 | const index = to.conversionsTo.indexOf(existingConversion) 1932 | to.conversionsTo.splice(index, 1) 1933 | } 1934 | 1935 | /** 1936 | * Produce the specific signature that a typed function 1937 | * will execute on the given arguments. Here, a "signature" is an 1938 | * object with properties 'params', 'test', 'fn', and 'implementation'. 1939 | * This last property is a function that converts params as necessary 1940 | * and then calls 'fn'. Returns null if there is no matching signature. 1941 | * @param {typed-function} tf 1942 | * @param {any[]} argList 1943 | * @returns {{params: string, test: function, fn: function, implementation: function}} 1944 | */ 1945 | typed.resolve = function (tf, argList) { 1946 | if (!isTypedFunction(tf)) { 1947 | throw new TypeError(NOT_TYPED_FUNCTION) 1948 | } 1949 | const sigs = tf._typedFunctionData.signatures 1950 | for (let i = 0; i < sigs.length; ++i) { 1951 | if (sigs[i].test(argList)) { 1952 | return sigs[i] 1953 | } 1954 | } 1955 | return null 1956 | } 1957 | 1958 | return typed 1959 | } 1960 | 1961 | export default create() 1962 | -------------------------------------------------------------------------------- /test-lib/apps/cjsApp.cjs: -------------------------------------------------------------------------------- 1 | const typed = require('../../lib/umd/typed-function.js') 2 | 3 | // create a typed function 4 | const fn1 = typed({ 5 | 'number, string': function (a, b) { 6 | return 'a is a number, b is a string' 7 | } 8 | }) 9 | 10 | // use the function 11 | // outputs 'a is a number, b is a string' 12 | const result = fn1(2, 'foo') 13 | console.log(result) 14 | -------------------------------------------------------------------------------- /test-lib/apps/esmApp.mjs: -------------------------------------------------------------------------------- 1 | import typed from '../../lib/esm/typed-function.mjs' 2 | 3 | // create a typed function 4 | const fn1 = typed({ 5 | 'number, string': function (a, b) { 6 | return 'a is a number, b is a string' 7 | } 8 | }) 9 | 10 | // use the function 11 | // outputs 'a is a number, b is a string' 12 | const result = fn1(2, 'foo') 13 | console.log(result) 14 | -------------------------------------------------------------------------------- /test-lib/lib.test.cjs: -------------------------------------------------------------------------------- 1 | const { strictEqual } = require('assert') 2 | const cp = require('child_process') 3 | const path = require('path') 4 | 5 | describe('lib', () => { 6 | it('should load the library using ESM', (done) => { 7 | const filename = path.join(__dirname, 'apps/esmApp.mjs') 8 | 9 | cp.exec(`node ${filename}`, function (error, result) { 10 | strictEqual(error, null) 11 | strictEqual(result, 'a is a number, b is a string\n') 12 | done() 13 | }) 14 | }) 15 | 16 | it('should load the library using CJS (using dynamic import)', (done) => { 17 | const filename = path.join(__dirname, 'apps/cjsApp.cjs') 18 | 19 | cp.exec(`node ${filename}`, function (error, result) { 20 | strictEqual(error, null) 21 | strictEqual(result, 'a is a number, b is a string\n') 22 | done() 23 | }) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/any_type.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import typed from '../src/typed-function.mjs' 3 | 4 | describe('any type', function () { 5 | it('should compose a function with one any type argument', function () { 6 | const fn = typed({ 7 | any: function (value) { 8 | return 'any:' + value 9 | }, 10 | string: function (value) { 11 | return 'string:' + value 12 | }, 13 | boolean: function (value) { 14 | return 'boolean:' + value 15 | } 16 | }) 17 | 18 | assert(fn.signatures instanceof Object) 19 | assert.strictEqual(Object.keys(fn.signatures).length, 3) 20 | assert.equal(fn(2), 'any:2') 21 | assert.equal(fn([1, 2, 3]), 'any:1,2,3') 22 | assert.equal(fn('foo'), 'string:foo') 23 | assert.equal(fn(false), 'boolean:false') 24 | }) 25 | 26 | it('should compose a function with multiple any type arguments (1)', function () { 27 | const fn = typed({ 28 | 'any,boolean': function () { 29 | return 'any,boolean' 30 | }, 31 | 'any,string': function () { 32 | return 'any,string' 33 | } 34 | }) 35 | 36 | assert(fn.signatures instanceof Object) 37 | assert.strictEqual(Object.keys(fn.signatures).length, 2) 38 | assert.equal(fn([], true), 'any,boolean') 39 | assert.equal(fn(2, 'foo'), 'any,string') 40 | assert.throws(function () { fn([], new Date()) }, /TypeError: Unexpected type of argument in function unnamed \(expected: string or boolean, actual: Date, index: 1\)/) 41 | assert.throws(function () { fn(2, 2) }, /TypeError: Unexpected type of argument in function unnamed \(expected: string or boolean, actual: number, index: 1\)/) 42 | assert.throws(function () { fn(2) }, /TypeError: Too few arguments in function unnamed \(expected: string or boolean, index: 1\)/) 43 | }) 44 | 45 | it('should compose a function with multiple any type arguments (2)', function () { 46 | const fn = typed({ 47 | 'any,boolean': function () { 48 | return 'any,boolean' 49 | }, 50 | 'any,number': function () { 51 | return 'any,number' 52 | }, 53 | 'string,any': function () { 54 | return 'string,any' 55 | } 56 | }) 57 | 58 | assert(fn.signatures instanceof Object) 59 | assert.strictEqual(Object.keys(fn.signatures).length, 3) 60 | assert.equal(fn([], true), 'any,boolean') 61 | assert.equal(fn([], 2), 'any,number') 62 | assert.equal(fn('foo', 2), 'string,any') 63 | assert.throws(function () { fn([], new Date()) }, /TypeError: Unexpected type of argument in function unnamed \(expected: number or boolean, actual: Date, index: 1\)/) 64 | assert.throws(function () { fn([], 'foo') }, /TypeError: Unexpected type of argument in function unnamed \(expected: number or boolean, actual: string, index: 1\)/) 65 | }) 66 | 67 | it('should compose a function with multiple any type arguments (3)', function () { 68 | const fn = typed({ 69 | 'string,any': function () { 70 | return 'string,any' 71 | }, 72 | any: function () { 73 | return 'any' 74 | } 75 | }) 76 | 77 | assert(fn.signatures instanceof Object) 78 | assert.equal(Object.keys(fn.signatures).length, 2) 79 | assert.ok('any' in fn.signatures) 80 | assert.ok('string,any' in fn.signatures) 81 | assert.equal(fn('foo', 2), 'string,any') 82 | assert.equal(fn([]), 'any') 83 | assert.equal(fn('foo'), 'any') 84 | assert.throws(function () { fn() }, /TypeError: Too few arguments in function unnamed \(expected: any, index: 0\)/) 85 | assert.throws(function () { fn([], 'foo') }, /TypeError: Too many arguments in function unnamed \(expected: 1, actual: 2\)/) 86 | assert.throws(function () { fn('foo', 4, []) }, /TypeError: Too many arguments in function unnamed \(expected: 2, actual: 3\)/) 87 | }) 88 | 89 | it('should compose a function with multiple any type arguments (4)', function () { 90 | const fn = typed('fn1', { 91 | 'number,number': function () { 92 | return 'number,number' 93 | }, 94 | 'any,string': function () { 95 | return 'any,string' 96 | } 97 | }) 98 | 99 | assert(fn.signatures instanceof Object) 100 | assert.strictEqual(Object.keys(fn.signatures).length, 2) 101 | assert.equal(fn(2, 2), 'number,number') 102 | assert.equal(fn(2, 'foo'), 'any,string') 103 | assert.throws(function () { fn('foo') }, /TypeError: Too few arguments in function fn1 \(expected: string, index: 1\)/) 104 | assert.throws(function () { fn(1, 2, 3) }, /TypeError: Too many arguments in function fn1 \(expected: 2, actual: 3\)/) 105 | }) 106 | 107 | it('should compose a function with multiple any type arguments (5)', function () { 108 | const fn = typed({ 109 | 'string,string': function () { 110 | return 'string,string' 111 | }, 112 | any: function () { 113 | return 'any' 114 | } 115 | }) 116 | 117 | assert(fn.signatures instanceof Object) 118 | assert.strictEqual(Object.keys(fn.signatures).length, 2) 119 | assert.equal(fn('foo', 'bar'), 'string,string') 120 | assert.equal(fn([]), 'any') 121 | assert.equal(fn('foo'), 'any') 122 | assert.throws(function () { fn('foo', 'bar', 5) }, /TypeError: Too many arguments in function unnamed \(expected: 2, actual: 3\)/) 123 | assert.throws(function () { fn('foo', 2, 5) }, /TypeError: Unexpected type of argument in function unnamed \(expected: string, actual: number, index: 1\)/) 124 | assert.throws(function () { fn('foo', 'bar', 5) }, /TypeError: Too many arguments in function unnamed \(expected: 2, actual: 3\)/) 125 | }) 126 | 127 | it('var arg any type arguments should only handle unmatched types', function () { 128 | const fn = typed({ 129 | 'Array,string': function () { 130 | return 'Array,string' 131 | }, 132 | '...': function () { 133 | return 'any' 134 | } 135 | }) 136 | 137 | assert.equal(fn([], 'foo'), 'Array,string') 138 | assert.equal(fn([], 'foo', 'bar'), 'any') 139 | assert.equal(fn('string'), 'any') 140 | assert.equal(fn(2), 'any') 141 | assert.equal(fn(2, 3, 4), 'any') 142 | assert.equal(fn([]), 'any') 143 | assert.throws(function () { fn() }, /TypeError: Too few arguments in function unnamed \(expected: any, index: 0\)/) 144 | }) 145 | 146 | it('multiple use of any', function () { 147 | const fn = typed({ 148 | 'number,number': function () { 149 | return 'numbers' 150 | }, 151 | 'any,any': function () { 152 | return 'any' 153 | } 154 | }) 155 | 156 | assert(fn.signatures instanceof Object) 157 | assert.strictEqual(Object.keys(fn.signatures).length, 2) 158 | assert.equal(fn('a', 'b'), 'any') 159 | assert.equal(fn(1, 1), 'numbers') 160 | assert.equal(fn(1, 'b'), 'any') 161 | assert.equal(fn('a', 1), 'any') 162 | }) 163 | 164 | it('use one any in combination with vararg', function () { 165 | const fn = typed({ 166 | number: function () { 167 | return 'numbers' 168 | }, 169 | 'any,...any': function () { 170 | return 'any' 171 | } 172 | }) 173 | 174 | assert(fn.signatures instanceof Object) 175 | assert.strictEqual(Object.keys(fn.signatures).length, 2) 176 | assert.equal(fn('a', 'b'), 'any') 177 | assert.equal(fn(1), 'numbers') 178 | assert.equal(fn(1, 'b'), 'any') 179 | assert.equal(fn('a', 2), 'any') 180 | assert.equal(fn(1, 2), 'any') 181 | assert.equal(fn(1, 2, 3), 'any') 182 | }) 183 | 184 | it('use multi-layered any in combination with vararg', function () { 185 | const fn = typed({ 186 | 'number,number': function () { 187 | return 'numbers' 188 | }, 189 | 'any,any,...any': function () { 190 | return 'any' 191 | } 192 | }) 193 | 194 | assert(fn.signatures instanceof Object) 195 | assert.strictEqual(Object.keys(fn.signatures).length, 2) 196 | assert.equal(fn('a', 'b', 'c'), 'any') 197 | assert.equal(fn(1, 2), 'numbers') 198 | assert.equal(fn(1, 'b', 2), 'any') 199 | assert.equal(fn('a', 2, 3), 'any') 200 | assert.equal(fn(1, 2, 3), 'any') 201 | }) 202 | 203 | it('should permit multi-layered use of any', function () { 204 | const fn = typed({ 205 | 'any,any': function () { 206 | return 'two' 207 | }, 208 | 'number,number,string': function () { 209 | return 'three' 210 | } 211 | }) 212 | 213 | assert(fn.signatures instanceof Object) 214 | assert.strictEqual(Object.keys(fn.signatures).length, 2) 215 | assert.equal(fn('a', 'b'), 'two') 216 | assert.equal(fn(1, 1), 'two') 217 | assert.equal(fn(1, 1, 'a'), 'three') 218 | assert.throws(function () { fn(1, 1, 1) }, /TypeError: Unexpected type of argument in function unnamed \(expected: string, actual: number, index: 2\)/) 219 | }) 220 | }) 221 | -------------------------------------------------------------------------------- /test/browserEsmBuild.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | browser test 5 | 6 | 7 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/browserSrc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | browser test 5 | 6 | 7 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/compose.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import typed from '../src/typed-function.mjs' 3 | 4 | describe('compose', function () { 5 | it('should create a composed function with multiple types per argument', function () { 6 | const fn = typed({ 7 | 'string | number, boolean': function () { return 'A' }, 8 | 'boolean, boolean | number': function () { return 'B' }, 9 | string: function () { return 'C' } 10 | }) 11 | 12 | assert.equal(fn('str', false), 'A') 13 | assert.equal(fn(2, true), 'A') 14 | assert.equal(fn(false, true), 'B') 15 | assert.equal(fn(false, 2), 'B') 16 | assert.equal(fn('str'), 'C') 17 | assert.throws(function () { fn() }, /TypeError: Too few arguments in function unnamed \(expected: string or number or boolean, index: 0\)/) 18 | assert.throws(function () { fn(1, 2, 3) }, /TypeError: Unexpected type of argument in function unnamed \(expected: boolean, actual: number, index: 1\)/) 19 | assert.throws(function () { fn('str', 2) }, /TypeError: Unexpected type of argument in function unnamed \(expected: boolean, actual: number, index: 1\)/) 20 | assert.throws(function () { fn(true, 'str') }, /TypeError: Unexpected type of argument in function unnamed \(expected: number or boolean, actual: string, index: 1\)/) 21 | assert.throws(function () { fn(2, 3) }, /TypeError: Unexpected type of argument in function unnamed \(expected: boolean, actual: number, index: 1\)/) 22 | assert.throws(function () { fn(2, 'str') }, /TypeError: Unexpected type of argument in function unnamed \(expected: boolean, actual: string, index: 1\)/) 23 | }) 24 | 25 | // TODO: test whether the constructor throws errors when providing wrong arguments to typed(...) 26 | 27 | it('should compose a function with one argument', function () { 28 | const signatures = { 29 | number: function (value) { 30 | return 'number:' + value 31 | }, 32 | string: function (value) { 33 | return 'string:' + value 34 | }, 35 | boolean: function (value) { 36 | return 'boolean:' + value 37 | } 38 | } 39 | const fn = typed(signatures) 40 | 41 | assert.equal(fn(2), 'number:2') 42 | assert.equal(fn('foo'), 'string:foo') 43 | assert.equal(fn(false), 'boolean:false') 44 | assert(fn.signatures instanceof Object) 45 | assert.strictEqual(Object.keys(fn.signatures).length, 3) 46 | assert.strictEqual(fn.signatures.number, signatures.number) 47 | assert.strictEqual(fn.signatures.string, signatures.string) 48 | assert.strictEqual(fn.signatures.boolean, signatures.boolean) 49 | }) 50 | 51 | it('should compose a function with multiple arguments', function () { 52 | const signatures = { 53 | number: function (value) { 54 | return 'number:' + value 55 | }, 56 | string: function (value) { 57 | return 'string:' + value 58 | }, 59 | 'number, boolean': function (a, b) { // mind space after the comma, should be normalized by composer 60 | return 'number,boolean:' + a + ',' + b 61 | } 62 | } 63 | const fn = typed(signatures) 64 | 65 | assert.equal(fn(2), 'number:2') 66 | assert.equal(fn('foo'), 'string:foo') 67 | assert.equal(fn(2, false), 'number,boolean:2,false') 68 | assert(fn.signatures instanceof Object) 69 | assert.strictEqual(Object.keys(fn.signatures).length, 3) 70 | assert.strictEqual(fn.signatures.number, signatures.number) 71 | assert.strictEqual(fn.signatures.string, signatures.string) 72 | assert.strictEqual(fn.signatures['number,boolean'], signatures['number, boolean']) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /test/construction.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import typed from '../src/typed-function.mjs' 3 | 4 | describe('construction', function () { 5 | it('should throw an error when not providing any arguments', function () { 6 | assert.throws(function () { 7 | typed() 8 | }, /Error: No signatures provided/) 9 | }) 10 | 11 | it('should throw an error when not providing any signatures', function () { 12 | assert.throws(function () { 13 | typed({}) 14 | }, /Error: Argument .*typed.* 0 .* not/) 15 | }) 16 | 17 | it('should create a named function', function () { 18 | const fn = typed('myFunction', { 19 | string: function (str) { 20 | return 'foo' 21 | } 22 | }) 23 | 24 | assert.equal(fn('bar'), 'foo') 25 | assert.equal(fn.name, 'myFunction') 26 | }) 27 | 28 | it('should create a typed function from a regular function with a signature', function () { 29 | function myFunction (str) { 30 | return 'foo' 31 | } 32 | myFunction.signature = 'string' 33 | 34 | const fn = typed(myFunction) 35 | 36 | assert.equal(fn('bar'), 'foo') 37 | assert.equal(fn.name, 'myFunction') 38 | assert.deepEqual(Object.keys(fn.signatures), ['string']) 39 | }) 40 | 41 | it('should create an unnamed function', function () { 42 | const fn = typed({ 43 | string: function (str) { 44 | return 'foo' 45 | } 46 | }) 47 | 48 | assert.equal(fn('bar'), 'foo') 49 | assert.equal(fn.name, '') 50 | }) 51 | 52 | it('should inherit the name of typed functions', function () { 53 | const fn = typed({ 54 | string: typed('fn1', { 55 | string: function (str) { 56 | return 'foo' 57 | } 58 | }) 59 | }) 60 | 61 | assert.equal(fn('bar'), 'foo') 62 | assert.equal(fn.name, 'fn1') 63 | }) 64 | 65 | it('should not inherit the name of the JavaScript functions (only from typed functions)', function () { 66 | const fn = typed({ 67 | string: function fn1 (str) { 68 | return 'foo' 69 | } 70 | }) 71 | 72 | assert.equal(fn('bar'), 'foo') 73 | assert.equal(fn.name, '') 74 | }) 75 | 76 | it('should throw if attempting to construct from other types', () => { 77 | assert.throws(() => typed(1), TypeError) 78 | assert.throws(() => typed('myfunc', 'implementation'), TypeError) 79 | }) 80 | 81 | it('should compose a function with zero arguments', function () { 82 | const signatures = { 83 | '': function () { 84 | return 'noargs' 85 | } 86 | } 87 | const fn = typed(signatures) 88 | 89 | assert.equal(fn(), 'noargs') 90 | assert(fn.signatures instanceof Object) 91 | assert.strictEqual(Object.keys(fn.signatures).length, 1) 92 | assert.strictEqual(fn.signatures[''], signatures['']) 93 | }) 94 | 95 | it('should create a typed function with one argument', function () { 96 | const fn = typed({ 97 | string: function () { 98 | return 'string' 99 | } 100 | }) 101 | 102 | assert.equal(fn('hi'), 'string') 103 | }) 104 | 105 | it('should ignore whitespace when creating a typed function with one argument', function () { 106 | const fn = typed({ ' ... string ': A => 'string' }) 107 | assert.equal(fn('hi'), 'string') 108 | }) 109 | 110 | it('should create a typed function with two arguments', function () { 111 | const fn = typed({ 112 | 'string, boolean': function () { 113 | return 'foo' 114 | } 115 | }) 116 | 117 | assert.equal(fn('hi', true), 'foo') 118 | }) 119 | 120 | it('should create a named, typed function', function () { 121 | const fn = typed('myFunction', { 122 | 'string, boolean': function () { 123 | return 'noargs' 124 | } 125 | }) 126 | 127 | assert.equal(fn('hi', true), 'noargs') 128 | assert.equal(fn.name, 'myFunction') 129 | }) 130 | 131 | it('should correctly recognize Date from Object (both are an Object)', function () { 132 | const signatures = { 133 | Object: function (value) { 134 | assert(value instanceof Object) 135 | return 'Object' 136 | }, 137 | Date: function (value) { 138 | assert(value instanceof Date) 139 | return 'Date' 140 | } 141 | } 142 | const fn = typed(signatures) 143 | 144 | assert.equal(fn({ foo: 'bar' }), 'Object') 145 | assert.equal(fn(new Date()), 'Date') 146 | }) 147 | 148 | it('should correctly handle null', function () { 149 | const fn = typed({ 150 | Object: function (a) { 151 | return 'Object' 152 | }, 153 | null: function (a) { 154 | return 'null' 155 | }, 156 | undefined: function (a) { 157 | return 'undefined' 158 | } 159 | }) 160 | 161 | assert.equal(fn({}), 'Object') 162 | assert.equal(fn(null), 'null') 163 | assert.equal(fn(undefined), 'undefined') 164 | }) 165 | 166 | it('should throw correct error message when passing null from an Object', function () { 167 | const signatures = { 168 | Object: function (value) { 169 | assert(value instanceof Object) 170 | return 'Object' 171 | } 172 | } 173 | const fn = typed(signatures) 174 | 175 | assert.equal(fn({}), 'Object') 176 | assert.throws(function () { fn(null) }, 177 | /TypeError: Unexpected type of argument in function unnamed \(expected: Object, actual: null, index: 0\)/) 178 | }) 179 | 180 | it('should create a new, isolated instance of typed-function', function () { 181 | const typed1 = typed.create() 182 | const typed2 = typed.create() 183 | function Person () {} 184 | 185 | typed1.addType({ 186 | name: 'Person', 187 | test: function (x) { 188 | return x instanceof Person 189 | } 190 | }) 191 | 192 | assert.strictEqual(typed.create, typed1.create) 193 | assert.notStrictEqual(typed.addTypes, typed1.addTypes) 194 | assert.notStrictEqual(typed.addConversion, typed1.addConversion) 195 | 196 | assert.strictEqual(typed.create, typed2.create) 197 | assert.notStrictEqual(typed.addTypes, typed2.addTypes) 198 | assert.notStrictEqual(typed.addConversion, typed2.addConversion) 199 | 200 | assert.strictEqual(typed1.create, typed2.create) 201 | assert.notStrictEqual(typed1.addTypes, typed2.addTypes) 202 | assert.notStrictEqual(typed1.addConversion, typed2.addConversion) 203 | 204 | typed1({ 205 | Person: function (p) { return 'Person' } 206 | }) 207 | 208 | assert.throws(function () { 209 | typed2({ 210 | Person: function (p) { return 'Person' } 211 | }) 212 | }, /Error: Unknown type "Person"/) 213 | }) 214 | 215 | it('should add a type using addType (before object)', function () { 216 | const typed2 = typed.create() 217 | function Person () {} 218 | 219 | const newType = { 220 | name: 'Person', 221 | test: function (x) { 222 | return x instanceof Person 223 | } 224 | } 225 | 226 | const objectIndex = typed2._findType('Object').index 227 | typed2.addType(newType) 228 | assert.strictEqual(typed2._findType('Person').index, objectIndex) 229 | }) 230 | 231 | it('should add a type using addType at the end (after Object)', function () { 232 | const typed2 = typed.create() 233 | function Person () {} 234 | 235 | const newType = { 236 | name: 'Person', 237 | test: function (x) { 238 | return x instanceof Person 239 | } 240 | } 241 | 242 | typed2.addType(newType, false) 243 | 244 | assert.strictEqual( 245 | typed2._findType('Person').index, 246 | typed2._findType('any').index - 1) 247 | }) 248 | 249 | it('should add a type using addType (no object)', function () { 250 | const typed3 = typed.create() 251 | typed3.clear() 252 | typed3.addType({ name: 'number', test: n => typeof n === 'number' }) 253 | assert.strictEqual(typed3._findType('number').index, 0) 254 | }) 255 | 256 | it('should throw an error when passing an invalid type to addType', function () { 257 | const typed2 = typed.create() 258 | const errMsg = /TypeError: Object with properties {name: string, test: function} expected/ 259 | 260 | assert.throws(function () { typed2.addType({}) }, errMsg) 261 | assert.throws(function () { typed2.addType({ name: 2, test: function () {} }) }, errMsg) 262 | assert.throws(function () { typed2.addType({ name: 'foo', test: 'bar' }) }, errMsg) 263 | }) 264 | 265 | it('should throw an error when providing an unsupported type of argument', function () { 266 | const fn = typed('fn1', { 267 | number: function (value) { 268 | return 'number:' + value 269 | } 270 | }) 271 | 272 | assert.throws(function () { fn(new Date()) }, /TypeError: Unexpected type of argument in function fn1 \(expected: number, actual: Date, index: 0\)/) 273 | }) 274 | 275 | it('should throw an error when providing a wrong function signature', function () { 276 | const fn = typed('fn1', { 277 | number: function (value) { 278 | return 'number:' + value 279 | } 280 | }) 281 | 282 | assert.throws(function () { fn(1, 2) }, /TypeError: Too many arguments in function fn1 \(expected: 1, actual: 2\)/) 283 | }) 284 | 285 | it('should throw an error when composing with an unknown type', function () { 286 | assert.throws(function () { 287 | typed({ 288 | foo: function (value) { 289 | return 'number:' + value 290 | } 291 | }) 292 | }, /Error: Unknown type "foo"/) 293 | }) 294 | 295 | it('should give a hint when composing with a wrongly cased type', function () { 296 | assert.throws(function () { 297 | typed({ 298 | array: function (value) { 299 | return 'array:' + value 300 | } 301 | }) 302 | }, /Error: Unknown type "array". Did you mean "Array"?/) 303 | 304 | assert.throws(function () { 305 | typed({ 306 | function: function (value) { 307 | return 'Function:' + value 308 | } 309 | }) 310 | }, /Error: Unknown type "function". Did you mean "Function"?/) 311 | }) 312 | 313 | it('should attach signatures to the created typed-function', function () { 314 | const fn1 = function () {} 315 | const fn2 = function () {} 316 | const fn3 = function () {} 317 | const fn4 = function () {} 318 | 319 | const fn = typed({ 320 | string: fn1, 321 | 'string, boolean': fn2, 322 | 'number | Date, boolean': fn3, 323 | 'Array | Object, string | RegExp': fn3, 324 | 'number, ...string | number': fn4 325 | }) 326 | 327 | assert.deepStrictEqual(fn.signatures, { 328 | string: fn1, 329 | 'string,boolean': fn2, 330 | 'number,boolean': fn3, 331 | 'Date,boolean': fn3, 332 | 'Array,string': fn3, 333 | 'Array,RegExp': fn3, 334 | 'Object,string': fn3, 335 | 'Object,RegExp': fn3, 336 | 'number,...string|number': fn4 337 | }) 338 | }) 339 | 340 | it('should correctly order signatures', function () { 341 | const t2 = typed.create() 342 | t2.clear() 343 | t2.addTypes([ 344 | { name: 'foo', test: x => x[0] === 1 }, 345 | { name: 'bar', test: x => x[1] === 1 }, 346 | { name: 'baz', test: x => x[2] === 1 } 347 | ]) 348 | const fn = t2({ 349 | baz: a => 'isbaz', 350 | bar: a => 'isbar', 351 | foo: a => 'isfoo' 352 | }) 353 | 354 | assert.strictEqual(fn([1, 1, 1]), 'isfoo') 355 | assert.strictEqual(fn([0, 1, 1]), 'isbar') 356 | assert.strictEqual(fn([0, 0, 1]), 'isbaz') 357 | }) 358 | 359 | it('should increment the count of typed functions', function () { 360 | const saveCount = typed.createCount 361 | typed({ number: () => true }) 362 | assert.strictEqual(typed.createCount - saveCount, 1) 363 | }) 364 | 365 | it('should allow a function refer to itself', function () { 366 | const fn = typed({ 367 | number: function (value) { 368 | return 'number:' + value 369 | }, 370 | string: typed.referToSelf((self) => { 371 | return function (value) { 372 | assert.strictEqual(self, fn) 373 | 374 | return self(parseInt(value, 10)) 375 | } 376 | }) 377 | }) 378 | 379 | assert.equal(fn('2'), 'number:2') 380 | }) 381 | 382 | it('should allow to resolve multiple function signatures with referTo', function () { 383 | const fnNumber = function (value) { 384 | return 'number:' + value 385 | } 386 | 387 | const fnBoolean = function (value) { 388 | return 'boolean:' + value 389 | } 390 | 391 | const fn = typed({ 392 | number: fnNumber, 393 | boolean: fnBoolean, 394 | string: typed.referTo('number', 'boolean', (fnNumberResolved, fnBooleanResolved) => { 395 | assert.strictEqual(fnNumberResolved, fnNumber) 396 | assert.strictEqual(fnBooleanResolved, fnBoolean) 397 | 398 | return function fnString (value) { 399 | return fnNumberResolved(parseInt(value, 10)) 400 | } 401 | }) 402 | }) 403 | 404 | assert.equal(fn('2'), 'number:2') 405 | }) 406 | 407 | it('should resolve referTo signatures on the resolved signatures, not exact matches', function () { 408 | const fnNumberOrBoolean = function (value) { 409 | return 'number or boolean:' + value 410 | } 411 | 412 | const fn = typed({ 413 | 'number|boolean': fnNumberOrBoolean, 414 | string: typed.referTo('number', (fnNumberResolved) => { 415 | assert.strictEqual(fnNumberResolved, fnNumberOrBoolean) 416 | 417 | return function fnString (value) { 418 | return fnNumberResolved(parseInt(value, 10)) 419 | } 420 | }) 421 | }) 422 | 423 | assert.equal(fn('2'), 'number or boolean:2') 424 | }) 425 | 426 | it('should throw an exception when a signature is not found with referTo', function () { 427 | assert.throws(() => { 428 | typed({ 429 | string: typed.referTo('number', (fnNumberResolved) => { 430 | return function fnString (value) { 431 | return fnNumberResolved(parseInt(value, 10)) 432 | } 433 | }) 434 | }) 435 | }, /Error:.*reference.*signature "number"/) 436 | }) 437 | 438 | it('should allow forward references with referTo', function () { 439 | const forward = typed({ 440 | string: typed.referTo('number', (fnNumberResolved) => { 441 | return function fnString (value) { 442 | return fnNumberResolved(parseInt(value, 10)) 443 | } 444 | }), 445 | // Forward reference: we define `number` after we use it in `string` 446 | number: typed.referTo(() => { 447 | return value => 'number:' + value 448 | }) 449 | }) 450 | assert.strictEqual(forward('10'), 'number:10') 451 | }) 452 | 453 | it('should throw an exception in case of circular referTo', function () { 454 | assert.throws( 455 | () => { 456 | typed({ 457 | string: typed.referTo('number', fN => s => fN(s.length)), 458 | number: typed.referTo('string', fS => n => fS(n.toString())) 459 | }) 460 | }, 461 | SyntaxError) 462 | }) 463 | 464 | it('should throw with circular referTo and direct referToSelf', function () { 465 | assert.throws( 466 | () => { 467 | typed({ 468 | boolean: typed.referToSelf(self => b => b ? self(1) : self('false')), 469 | string: typed.referTo('number', fN => s => fN(s.length)), 470 | number: typed.referTo('string', fS => n => fS(n.toString())) 471 | }) 472 | }, 473 | SyntaxError) 474 | }) 475 | 476 | it('should throw an exception when a signature in referTo is not a string', function () { 477 | assert.throws(() => { 478 | typed.referTo(123, () => {}) 479 | }, /TypeError: Signatures must be strings/) 480 | 481 | assert.throws(() => { 482 | typed.referTo('number', 123, () => {}) 483 | }, /TypeError: Signatures must be strings/) 484 | }) 485 | 486 | it('should throw an exception when the last argument of referTo is not a callback function', function () { 487 | assert.throws(() => { 488 | typed.referTo('number') 489 | }, /TypeError: Callback function expected as last argument/) 490 | }) 491 | 492 | it('should throw an exception when the first argument of referToSelf is not a callback function', function () { 493 | assert.throws(() => { 494 | typed.referToSelf(123) 495 | }, /TypeError: Callback function expected as first argument/) 496 | }) 497 | 498 | it('should have correct context `this` when resolving reference function signatures', function () { 499 | // to make this work, in all functions we must use regular functions and no arrow functions, 500 | // and we need to use .call or .apply, passing the `this` context along 501 | const fnNumber = function (value) { 502 | return 'number:' + value + ', this.value:' + (this && this.value) 503 | } 504 | 505 | const fn = typed({ 506 | number: typed.referTo(function () { 507 | // created as a "reference" function just for the unit test... 508 | return fnNumber 509 | }), 510 | string: typed.referTo('number', function (fnNumberResolved) { 511 | assert.strictEqual(fnNumberResolved, fnNumber) 512 | 513 | return function fnString (value) { 514 | return fnNumberResolved.call(this, parseInt(value, 10)) 515 | } 516 | }) 517 | }) 518 | 519 | assert.equal(fn('2'), 'number:2, this.value:undefined') 520 | 521 | // verify the reference function has the right context 522 | const obj = { 523 | value: 42, 524 | fn 525 | } 526 | assert.equal(obj.fn('2'), 'number:2, this.value:42') 527 | }) 528 | 529 | it('should pass this function context', () => { 530 | const getProperty = typed({ 531 | string: function (key) { 532 | return this && this[key] 533 | } 534 | }) 535 | 536 | assert.equal(getProperty('value'), undefined) 537 | 538 | const obj = { 539 | value: 42, 540 | getProperty 541 | } 542 | 543 | assert.equal(obj.getProperty('value'), 42) 544 | 545 | const boundGetProperty = getProperty.bind({ otherValue: 123 }) 546 | assert.equal(boundGetProperty('otherValue'), 123) 547 | }) 548 | 549 | it('should throw a deprecation warning when self reference via `this(...)` is used', () => { 550 | assert.throws(() => { 551 | typed({ 552 | number: function (value) { 553 | return value * value 554 | }, 555 | string: function (value) { 556 | return this(parseFloat(value)) 557 | } 558 | }) 559 | }, /SyntaxError: Using `this` to self-reference a function is deprecated since typed-function@3\. Use typed\.referTo and typed\.referToSelf instead\./) 560 | }) 561 | 562 | it('should not throw a deprecation warning on `this(...)` when the warning is turned off', () => { 563 | const typed2 = typed.create() 564 | typed2.warnAgainstDeprecatedThis = false 565 | 566 | const deprecatedSquare = typed2({ 567 | number: function (value) { 568 | return value * value 569 | }, 570 | string: function (value) { 571 | return this(parseFloat(value)) 572 | } 573 | }) 574 | 575 | assert.equal(deprecatedSquare(3), 9) 576 | 577 | assert.throws(() => { 578 | deprecatedSquare('3') 579 | }, /TypeError: this is not a function/) 580 | }) 581 | 582 | it('should throw a deprecation warning when self reference via `this.signatures` is used', () => { 583 | assert.throws(() => { 584 | typed({ 585 | number: function (value) { 586 | return value * value 587 | }, 588 | string: function (value) { 589 | return this.signatures.number(parseFloat(value)) 590 | } 591 | }) 592 | }, /SyntaxError: Using `this` to self-reference a function is deprecated since typed-function@3\. Use typed\.referTo and typed\.referToSelf instead\./) 593 | }) 594 | }) 595 | -------------------------------------------------------------------------------- /test/conversion.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import typed from '../src/typed-function.mjs' 3 | 4 | function convertBool(b) { 5 | return +b 6 | } 7 | 8 | describe('conversion', function () { 9 | before(function () { 10 | typed.addConversions([ 11 | { from: 'boolean', to: 'number', convert: convertBool }, 12 | { from: 'boolean', to: 'string', convert: function (x) { return x + '' } }, 13 | { from: 'number', to: 'string', convert: function (x) { return x + '' } }, 14 | { 15 | from: 'string', 16 | to: 'Date', 17 | convert: function (x) { 18 | const d = new Date(x) 19 | return isNaN(d.valueOf()) ? undefined : d 20 | }, 21 | fallible: true // TODO: not yet supported 22 | } 23 | ]) 24 | }) 25 | 26 | after(function () { 27 | // cleanup conversions 28 | typed.clearConversions() 29 | }) 30 | 31 | it('should add conversions to a function with one argument', function () { 32 | const fn = typed({ 33 | string: function (a) { 34 | return a 35 | } 36 | }) 37 | 38 | assert.equal(fn(2), '2') 39 | assert.equal(fn(false), 'false') 40 | assert.equal(fn('foo'), 'foo') 41 | }) 42 | 43 | it('should add a conversion using addConversion', function () { 44 | const typed2 = typed.create() 45 | 46 | const conversion = { 47 | from: 'number', 48 | to: 'string', 49 | convert: function (x) { 50 | return x + '' 51 | } 52 | } 53 | 54 | assert.strictEqual(typed2._findType('string').conversionsTo.length, 0) 55 | 56 | typed2.addConversion(conversion) 57 | 58 | assert.strictEqual(typed2._findType('string').conversionsTo.length, 1) 59 | assert.strictEqual( 60 | typed2._findType('string').conversionsTo[0].convert, 61 | conversion.convert) 62 | }) 63 | 64 | it('should throw an error when a conversion already existing when using addConversion', function () { 65 | const typed2 = typed.create() 66 | 67 | const conversionA = { from: 'number', to: 'string', convert: () => 'a' } 68 | const conversionB = { from: 'number', to: 'string', convert: () => 'b' } 69 | 70 | 71 | typed2.addConversion(conversionA) 72 | 73 | assert.throws(() => { 74 | typed2.addConversion(conversionB) 75 | }, /There is already a conversion/) 76 | 77 | assert.throws(() => { 78 | typed2.addConversion(conversionB, { override: false }) 79 | }, /There is already a conversion/) 80 | }) 81 | 82 | it('should override a conversion using addConversion', function () { 83 | const typed2 = typed.create() 84 | 85 | const conversionA = { from: 'number', to: 'string', convert: () => 'a' } 86 | const conversionB = { from: 'number', to: 'string', convert: () => 'b' } 87 | 88 | typed2.addConversion(conversionA) 89 | assert.strictEqual(typed2._findType('string').conversionsTo.length, 1) 90 | assert.strictEqual( 91 | typed2._findType('string').conversionsTo[0].convert, 92 | conversionA.convert) 93 | 94 | typed2.addConversion(conversionB, { override: true }) 95 | assert.strictEqual(typed2._findType('string').conversionsTo.length, 1) 96 | assert.strictEqual( 97 | typed2._findType('string').conversionsTo[0].convert, 98 | conversionB.convert) 99 | }) 100 | 101 | it('should override a conversion using addConversions', function () { 102 | const typed2 = typed.create() 103 | 104 | // we add an unrelated conversion to ensure we cannot misuse the internal .index 105 | // (which is more like an auto incrementing id) 106 | const conversionUnrelated = { from: 'string', to: 'boolean', convert: () => 'c' } 107 | typed2.addConversion(conversionUnrelated) 108 | 109 | const conversionA = { from: 'number', to: 'string', convert: () => 'a' } 110 | const conversionB = { from: 'number', to: 'string', convert: () => 'b' } 111 | 112 | typed2.addConversion(conversionA) 113 | assert.strictEqual(typed2._findType('string').conversionsTo.length, 1) 114 | assert.strictEqual( 115 | typed2._findType('string').conversionsTo[0].convert, 116 | conversionA.convert) 117 | 118 | typed2.addConversions([conversionB], { override: true }) 119 | assert.strictEqual(typed2._findType('string').conversionsTo.length, 1) 120 | assert.strictEqual( 121 | typed2._findType('string').conversionsTo[0].convert, 122 | conversionB.convert) 123 | }) 124 | 125 | it('should throw an error when passing an invalid conversion object to addConversion', function () { 126 | const typed2 = typed.create() 127 | const errMsg = /TypeError: Object with properties \{from: string, to: string, convert: function} expected/ 128 | 129 | assert.throws(function () { typed2.addConversion({}) }, errMsg) 130 | assert.throws(function () { typed2.addConversion({ from: 'number', to: 'string' }) }, errMsg) 131 | assert.throws(function () { typed2.addConversion({ from: 'number', convert: function () { } }) }, errMsg) 132 | assert.throws(function () { typed2.addConversion({ to: 'string', convert: function () { } }) }, errMsg) 133 | assert.throws(function () { typed2.addConversion({ from: 2, to: 'string', convert: function () { } }) }, errMsg) 134 | assert.throws(function () { typed2.addConversion({ from: 'number', to: 2, convert: function () { } }) }, errMsg) 135 | assert.throws(function () { typed2.addConversion({ from: 'number', to: 'string', convert: 'foo' }) }, errMsg) 136 | }) 137 | 138 | it('should throw an error when attempting to add a conversion to unknown type', function () { 139 | assert.throws(() => typed.addConversion({ 140 | from: 'number', 141 | to: 'garbage', 142 | convert: () => null 143 | }), /Unknown type/) 144 | }) 145 | 146 | it('should add conversions to a function with multiple arguments', function () { 147 | // note: we add 'string, string' first, and `string, number` afterwards, 148 | // to test whether the conversions are correctly ordered. 149 | const fn = typed({ 150 | 'string, string': function (a, b) { 151 | assert.equal(typeof a, 'string') 152 | assert.equal(typeof b, 'string') 153 | return 'string, string' 154 | }, 155 | 'string, number': function (a, b) { 156 | assert.equal(typeof a, 'string') 157 | assert.equal(typeof b, 'number') 158 | return 'string, number' 159 | } 160 | }) 161 | 162 | assert.equal(fn(true, false), 'string, number') 163 | assert.equal(fn(true, 2), 'string, number') 164 | assert.equal(fn(true, 'foo'), 'string, string') 165 | assert.equal(fn(2, false), 'string, number') 166 | assert.equal(fn(2, 3), 'string, number') 167 | assert.equal(fn(2, 'foo'), 'string, string') 168 | assert.equal(fn('foo', true), 'string, number') 169 | assert.equal(fn('foo', 2), 'string, number') 170 | assert.equal(fn('foo', 'foo'), 'string, string') 171 | assert.equal(Object.keys(fn.signatures).length, 2) 172 | assert.ok('string,number' in fn.signatures) 173 | assert.ok('string,string' in fn.signatures) 174 | }) 175 | 176 | it('should add conversions to a function with rest parameters (1)', function () { 177 | const toNumber = typed({ 178 | '...number': function (values) { 179 | assert(Array.isArray(values)) 180 | return values 181 | } 182 | }) 183 | 184 | assert.deepStrictEqual(toNumber(2, 3, 4), [2, 3, 4]) 185 | assert.deepStrictEqual(toNumber(2, true, 4), [2, 1, 4]) 186 | assert.deepStrictEqual(toNumber(1, 2, false), [1, 2, 0]) 187 | assert.deepStrictEqual(toNumber(1, 2, true), [1, 2, 1]) 188 | assert.deepStrictEqual(toNumber(true, 1, 2), [1, 1, 2]) 189 | assert.deepStrictEqual(toNumber(true, false, true), [1, 0, 1]) 190 | }) 191 | 192 | it('should add conversions to a function with rest parameters (2)', function () { 193 | const sum = typed({ 194 | 'string, ...number': function (name, values) { 195 | assert.equal(typeof name, 'string') 196 | assert(Array.isArray(values)) 197 | let sum = 0 198 | for (let i = 0; i < values.length; i++) { 199 | sum += values[i] 200 | } 201 | return sum 202 | } 203 | }) 204 | 205 | assert.equal(sum('foo', 2, 3, 4), 9) 206 | assert.equal(sum('foo', 2, true, 4), 7) 207 | assert.equal(sum('foo', 1, 2, false), 3) 208 | assert.equal(sum('foo', 1, 2, true), 4) 209 | assert.equal(sum('foo', true, 1, 2), 4) 210 | assert.equal(sum('foo', true, false, true), 2) 211 | assert.equal(sum(123, 2, 3), 5) 212 | assert.equal(sum(false, 2, 3), 5) 213 | }) 214 | 215 | it('should add conversions to a function with rest parameters in a non-conflicting way', function () { 216 | const fn = typed({ 217 | '...number': function (values) { 218 | return values 219 | }, 220 | boolean: function (value) { 221 | assert.equal(typeof value, 'boolean') 222 | return 'boolean' 223 | } 224 | }) 225 | 226 | assert.deepStrictEqual(fn(2, 3, 4), [2, 3, 4]) 227 | assert.deepStrictEqual(fn(2, true, 4), [2, 1, 4]) 228 | assert.deepStrictEqual(fn(true, 3, 4), [1, 3, 4]) 229 | assert.equal(fn(false), 'boolean') 230 | assert.equal(fn(true), 'boolean') 231 | }) 232 | 233 | it('should add conversions to a function with rest parameters in a non-conflicting way', function () { 234 | const typed2 = typed.create() 235 | typed2.addConversions([ 236 | { from: 'boolean', to: 'number', convert: function (x) { return +x } }, 237 | { from: 'string', to: 'number', convert: function (x) { return parseFloat(x) } }, 238 | { from: 'string', to: 'boolean', convert: function (x) { return !!x } } 239 | ]) 240 | 241 | // booleans can be converted to numbers, so the `...number` signature 242 | // will match. But the `...boolean` signature is a better (exact) match so that 243 | // should be picked 244 | const fn = typed2({ 245 | '...number': function (values) { 246 | return values 247 | }, 248 | '...boolean': function (values) { 249 | return values 250 | } 251 | }) 252 | 253 | assert.deepStrictEqual(fn(2, 3, 4), [2, 3, 4]) 254 | assert.deepStrictEqual(fn(2, true, 4), [2, 1, 4]) 255 | assert.deepStrictEqual(fn(true, true, true), [true, true, true]) 256 | }) 257 | 258 | it('should add conversions to a function with variable and union arguments', function () { 259 | const fn = typed({ 260 | '...string | number': function (values) { 261 | assert(Array.isArray(values)) 262 | return values 263 | } 264 | }) 265 | 266 | assert.deepStrictEqual(fn(2, 3, 4), [2, 3, 4]) 267 | assert.deepStrictEqual(fn(2, true, 4), [2, 1, 4]) 268 | assert.deepStrictEqual(fn(2, 'str'), [2, 'str']) 269 | assert.deepStrictEqual(fn('str', true, false), ['str', 1, 0]) 270 | assert.deepStrictEqual(fn('str', 2, false), ['str', 2, 0]) 271 | 272 | assert.throws(function () { fn(new Date(), '2') }, /TypeError: Unexpected type of argument in function unnamed \(expected: string or number or boolean, actual: Date, index: 0\)/) 273 | }) 274 | 275 | it('should order conversions and type Object correctly ', function () { 276 | const typed2 = typed.create() 277 | typed2.addConversion( 278 | { from: 'Date', to: 'string', convert: function (x) { return x.toISOString() } } 279 | ) 280 | 281 | const fn = typed2({ 282 | string: function () { 283 | return 'string' 284 | }, 285 | Object: function () { 286 | return 'object' 287 | } 288 | }) 289 | 290 | assert.equal(fn('foo'), 'string') 291 | assert.equal(fn(new Date(2018, 1, 20)), 'string') 292 | assert.equal(fn({ a: 2 }), 'object') 293 | }) 294 | 295 | it('should add non-conflicting conversions to a function with one argument', function () { 296 | const fn = typed({ 297 | number: function (a) { 298 | return a 299 | }, 300 | string: function (a) { 301 | return a 302 | } 303 | }) 304 | 305 | // booleans should be converted to number 306 | assert.strictEqual(fn(false), 0) 307 | assert.strictEqual(fn(true), 1) 308 | 309 | // numbers and strings should be left as is 310 | assert.strictEqual(fn(2), 2) 311 | assert.strictEqual(fn('foo'), 'foo') 312 | }) 313 | 314 | it('should add non-conflicting conversions to a function with one argument', function () { 315 | const fn = typed({ 316 | boolean: function (a) { 317 | return a 318 | } 319 | }) 320 | 321 | // booleans should be converted to number 322 | assert.equal(fn(false), 0) 323 | assert.equal(fn(true), 1) 324 | }) 325 | 326 | it('should add non-conflicting conversions to a function with two arguments', function () { 327 | const fn = typed({ 328 | 'boolean, boolean': function (a, b) { 329 | return 'boolean, boolean' 330 | }, 331 | 'number, number': function (a, b) { 332 | return 'number, number' 333 | } 334 | }) 335 | 336 | // console.log('FN', fn.toString()); 337 | 338 | // booleans should be converted to number 339 | assert.equal(fn(false, true), 'boolean, boolean') 340 | assert.equal(fn(2, 4), 'number, number') 341 | assert.equal(fn(false, 4), 'number, number') 342 | assert.equal(fn(2, true), 'number, number') 343 | }) 344 | 345 | it('should add non-conflicting conversions to a function with three arguments', function () { 346 | const fn = typed({ 347 | 'boolean, boolean, boolean': function (a, b, c) { 348 | return 'booleans' 349 | }, 350 | 'number, number, number': function (a, b, c) { 351 | return 'numbers' 352 | } 353 | }) 354 | 355 | // console.log('FN', fn.toString()); 356 | 357 | // booleans should be converted to number 358 | assert.equal(fn(false, true, true), 'booleans') 359 | assert.equal(fn(false, false, 5), 'numbers') 360 | assert.equal(fn(false, 4, false), 'numbers') 361 | assert.equal(fn(2, false, false), 'numbers') 362 | assert.equal(fn(false, 4, 5), 'numbers') 363 | assert.equal(fn(2, false, 5), 'numbers') 364 | assert.equal(fn(2, 4, false), 'numbers') 365 | assert.equal(fn(2, 4, 5), 'numbers') 366 | }) 367 | 368 | it('should only end up with one way to convert a signature', function () { 369 | const t2 = typed.create() 370 | t2.addConversions([ 371 | { from: 'number', to: 'string', convert: n => 'N' + n }, 372 | { from: 'Array', to: 'boolean', convert: A => A.length > 0 } 373 | ]) 374 | const ambiguous = t2({ 375 | 'string, Array': (s, A) => 'one ' + s, 376 | 'number, boolean': (n, b) => 'two' + n 377 | }) // Could be two ways to apply to 'number, Array'; want only one 378 | assert.strictEqual(ambiguous._typedFunctionData.signatures.length, 3) 379 | assert.strictEqual( 380 | t2.find(ambiguous, 'number, Array')(0, [0]), 381 | 'two0') 382 | }) 383 | 384 | it('should prefer conversions to any type argument', function () { 385 | const fn = typed({ 386 | number: function (a) { 387 | return 'number' 388 | }, 389 | any: function (a) { 390 | return 'any' 391 | } 392 | }) 393 | 394 | assert.equal(fn(2), 'number') 395 | assert.equal(fn(true), 'number') 396 | assert.equal(fn('foo'), 'any') 397 | assert.equal(fn('{}'), 'any') 398 | }) 399 | 400 | it('should allow removal of conversions', function () { 401 | const inc = typed({ number: n => n + 1 }) 402 | assert.strictEqual(inc(true), 2) 403 | typed.removeConversion({ 404 | from: 'boolean', 405 | to: 'number', 406 | convert: convertBool 407 | }) 408 | assert.throws(() => typed.convert(false, 'number'), /no conversions/) 409 | const dec = typed({ number: n => n - 1 }) 410 | assert.throws(() => dec(true), /TypeError: Unexpected type/) 411 | // But pre-existing functions remain OK: 412 | assert.strictEqual(inc(true), 2) 413 | }) 414 | 415 | describe('ordering', function () { 416 | it('should correctly select the signatures with the least amount of conversions', function () { 417 | typed.clearConversions() 418 | typed.addConversions([ 419 | { from: 'boolean', to: 'number', convert: function (x) { return +x } }, 420 | { from: 'number', to: 'string', convert: function (x) { return x + '' } }, 421 | { from: 'boolean', to: 'string', convert: function (x) { return x + '' } } 422 | ]) 423 | 424 | const fn = typed({ 425 | 'boolean, boolean': function (a, b) { 426 | assert.equal(typeof a, 'boolean') 427 | assert.equal(typeof b, 'boolean') 428 | return 'booleans' 429 | }, 430 | 'number, number': function (a, b) { 431 | assert.equal(typeof a, 'number') 432 | assert.equal(typeof b, 'number') 433 | return 'numbers' 434 | }, 435 | 'string, string': function (a, b) { 436 | assert.equal(typeof a, 'string') 437 | assert.equal(typeof b, 'string') 438 | return 'strings' 439 | } 440 | }) 441 | 442 | assert.equal(fn(true, true), 'booleans') 443 | assert.equal(fn(2, true), 'numbers') 444 | assert.equal(fn(true, 2), 'numbers') 445 | assert.equal(fn(2, 2), 'numbers') 446 | assert.equal(fn('foo', 'bar'), 'strings') 447 | assert.equal(fn('foo', 2), 'strings') 448 | assert.equal(fn(2, 'foo'), 'strings') 449 | assert.equal(fn(true, 'foo'), 'strings') 450 | assert.equal(fn('foo', true), 'strings') 451 | 452 | assert.equal(Object.keys(fn.signatures).length, 3) 453 | assert.ok('number,number' in fn.signatures) 454 | assert.ok('string,string' in fn.signatures) 455 | assert.ok('boolean,boolean' in fn.signatures) 456 | }) 457 | 458 | it('should select the signatures with the conversion with the lowest index (1)', function () { 459 | typed.clearConversions() 460 | typed.addConversions([ 461 | { from: 'boolean', to: 'string', convert: function (x) { return x + '' } }, 462 | { from: 'boolean', to: 'number', convert: function (x) { return x + 0 } } 463 | ]) 464 | 465 | // in the following typed function, a boolean input can be converted to 466 | // both a string or a number, which is both ok. In that case, 467 | // the conversion with the lowest index should be picked: boolean -> string 468 | const fn = typed({ 469 | 'string | number': function (a) { 470 | return a 471 | } 472 | }) 473 | 474 | assert.strictEqual(fn(true), 'true') 475 | 476 | assert.equal(Object.keys(fn.signatures).length, 2) 477 | assert.ok('number' in fn.signatures) 478 | assert.ok('string' in fn.signatures) 479 | }) 480 | 481 | it('should select the signatures with the conversion with the lowest index (2)', function () { 482 | typed.clearConversions() 483 | typed.addConversions([ 484 | { from: 'boolean', to: 'number', convert: function (x) { return x + 0 } }, 485 | { from: 'boolean', to: 'string', convert: function (x) { return x + '' } } 486 | ]) 487 | 488 | // in the following typed function, a boolean input can be converted to 489 | // both a string or a number, which is both ok. In that case, 490 | // the conversion with the lowest index should be picked: boolean -> number 491 | const fn = typed({ 492 | 'string | number': function (a) { 493 | return a 494 | } 495 | }) 496 | 497 | assert.strictEqual(fn(true), 1) 498 | }) 499 | 500 | it('should select the signatures with least needed conversions (1)', function () { 501 | typed.clearConversions() 502 | typed.addConversions([ 503 | { from: 'number', to: 'boolean', convert: function (x) { return !!x } }, 504 | { from: 'number', to: 'string', convert: function (x) { return x + '' } }, 505 | { from: 'boolean', to: 'string', convert: function (x) { return x + '' } } 506 | ]) 507 | 508 | // in the following typed function, the number input can be converted to 509 | // both a string or a boolean, which is both ok. It should pick the 510 | // conversion to boolean because that is defined first 511 | const fn = typed({ 512 | string: function (a) { return a }, 513 | boolean: function (a) { return a } 514 | }) 515 | 516 | assert.strictEqual(fn(1), true) 517 | }) 518 | 519 | it('should select the signatures with least needed conversions (2)', function () { 520 | typed.clearConversions() 521 | typed.addConversions([ 522 | { from: 'number', to: 'boolean', convert: function (x) { return !!x } }, 523 | { from: 'number', to: 'string', convert: function (x) { return x + '' } }, 524 | { from: 'boolean', to: 'string', convert: function (x) { return x + '' } } 525 | ]) 526 | 527 | // in the following typed function, the number input can be converted to 528 | // both a string or a boolean, which is both ok. It should pick the 529 | // conversion to boolean because that conversion is defined first 530 | const fn = typed({ 531 | 'number, number': function (a, b) { return [a, b] }, 532 | 'string, string': function (a, b) { return [a, b] }, 533 | 'boolean, boolean': function (a, b) { return [a, b] } 534 | }) 535 | 536 | assert.deepStrictEqual(fn('foo', 2), ['foo', '2']) 537 | assert.deepStrictEqual(fn(1, true), [true, true]) 538 | }) 539 | }) 540 | }) 541 | -------------------------------------------------------------------------------- /test/convert.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import typed from '../src/typed-function.mjs' 3 | 4 | describe('convert', function () { 5 | before(function () { 6 | typed.addConversions([ 7 | { from: 'boolean', to: 'number', convert: function (x) { return +x } }, 8 | { from: 'boolean', to: 'string', convert: function (x) { return x + '' } }, 9 | { from: 'number', to: 'string', convert: function (x) { return x + '' } }, 10 | { 11 | from: 'string', 12 | to: 'Date', 13 | convert: function (x) { 14 | const d = new Date(x) 15 | return isNaN(d.valueOf()) ? undefined : d 16 | }, 17 | fallible: true // TODO: not yet supported 18 | } 19 | ]) 20 | }) 21 | 22 | after(function () { 23 | // cleanup conversions 24 | typed.clearConversions() 25 | }) 26 | 27 | it('should convert a value', function () { 28 | assert.strictEqual(typed.convert(2, 'string'), '2') 29 | assert.strictEqual(typed.convert(true, 'string'), 'true') 30 | assert.strictEqual(typed.convert(true, 'number'), 1) 31 | }) 32 | 33 | it('should return same value when conversion is not needed', function () { 34 | assert.strictEqual(typed.convert(2, 'number'), 2) 35 | assert.strictEqual(typed.convert(true, 'boolean'), true) 36 | }) 37 | 38 | it('should throw an error when an unknown type is requested', function () { 39 | assert.throws(function () { typed.convert(2, 'foo') }, /Unknown type.*foo/) 40 | }) 41 | 42 | it('should throw an error when no conversion function is found', function () { 43 | assert.throws( 44 | function () { typed.convert(2, 'boolean') }, 45 | /no conversions to boolean/) 46 | assert.throws( 47 | function () { typed.convert(null, 'string') }, 48 | /Cannot convert null to string/) 49 | }) 50 | 51 | it('should pick the right conversion function when a value matches multiple types', () => { 52 | // based on https://github.com/josdejong/typed-function/issues/128 53 | const typed2 = typed.create() 54 | 55 | typed2.clear() 56 | typed2.addTypes([ 57 | { 58 | name: 'number', 59 | test: x => typeof x === 'number' 60 | }, 61 | { 62 | name: 'identifier', 63 | test: x => (typeof x === 'string' && 64 | /^\p{Alphabetic}[\d\p{Alphabetic}]*$/u.test(x)) 65 | }, 66 | { 67 | name: 'string', 68 | test: x => typeof x === 'string' 69 | }, 70 | { 71 | name: 'boolean', 72 | test: x => typeof x === 'boolean' 73 | } 74 | ]) 75 | 76 | typed2.addConversion({ from: 'string', to: 'number', convert: x => parseFloat(x) }) 77 | 78 | const check = typed2('check', { 79 | identifier: i => 'found an identifier: ' + i, 80 | string: s => s + ' is just a string' 81 | }) 82 | 83 | assert.strictEqual(check('xy33'), 'found an identifier: xy33') 84 | assert.strictEqual(check('Wow!'), 'Wow! is just a string') 85 | 86 | assert.strictEqual(typed2.convert('123.5', 'number'), 123.5) 87 | assert.strictEqual(typed2.convert('Infinity', 'number'), Infinity) 88 | 89 | const check2 = typed2({ boolean: () => 'yes' }) 90 | assert.throws(() => check2('x'), /TypeError:.*identifier.?|.?string/) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /test/errors.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import typed from '../src/typed-function.mjs' 3 | 4 | describe('errors', function () { 5 | it('should give correct error in case of too few arguments (named function)', function () { 6 | const fn = typed('fn1', { 'string, boolean': function () {} }) 7 | 8 | assert.throws(function () { fn() }, /TypeError: Too few arguments in function fn1 \(expected: string, index: 0\)/) 9 | assert.throws(function () { fn('foo') }, /TypeError: Too few arguments in function fn1 \(expected: boolean, index: 1\)/) 10 | }) 11 | 12 | it('should give correct error in case of too few arguments (unnamed function)', function () { 13 | const fn = typed({ 'string, boolean': function () {} }) 14 | 15 | assert.throws(function () { fn() }, /TypeError: Too few arguments in function unnamed \(expected: string, index: 0\)/) 16 | assert.throws(function () { fn('foo') }, /TypeError: Too few arguments in function unnamed \(expected: boolean, index: 1\)/) 17 | }) 18 | 19 | it('should give correct error in case of too few arguments (rest params)', function () { 20 | const fn = typed({ '...string': function () {} }) 21 | 22 | assert.throws(function () { fn() }, /TypeError: Too few arguments in function unnamed \(expected: string, index: 0\)/) 23 | }) 24 | 25 | it('should give correct error in case of too few arguments (rest params) (2)', function () { 26 | const fn = typed({ 'boolean, ...string': function () {} }) 27 | 28 | assert.throws(function () { fn() }, /TypeError: Too few arguments in function unnamed \(expected: boolean, index: 0\)/) 29 | assert.throws(function () { fn(true) }, /TypeError: Too few arguments in function unnamed \(expected: string, index: 1\)/) 30 | }) 31 | 32 | it('should give correct error in case of too many arguments (unnamed function)', function () { 33 | const fn = typed({ 'string, boolean': function () {} }) 34 | 35 | assert.throws(function () { fn('foo', true, 2) }, /TypeError: Too many arguments in function unnamed \(expected: 2, actual: 3\)/) 36 | assert.throws(function () { fn('foo', true, 2, 1) }, /TypeError: Too many arguments in function unnamed \(expected: 2, actual: 4\)/) 37 | }) 38 | 39 | it('should give correct error in case of too many arguments (named function)', function () { 40 | const fn = typed('fn2', { 'string, boolean': function () {} }) 41 | 42 | assert.throws(function () { fn('foo', true, 2) }, /TypeError: Too many arguments in function fn2 \(expected: 2, actual: 3\)/) 43 | assert.throws(function () { fn('foo', true, 2, 1) }, /TypeError: Too many arguments in function fn2 \(expected: 2, actual: 4\)/) 44 | }) 45 | 46 | it('should give correct error in case of wrong type of argument (unnamed function)', function () { 47 | const fn = typed({ boolean: function () {} }) 48 | 49 | assert.throws(function () { fn('foo') }, /TypeError: Unexpected type of argument in function unnamed \(expected: boolean, actual: string, index: 0\)/) 50 | }) 51 | 52 | it('should give correct error in case of wrong type of argument (named function)', function () { 53 | const fn = typed('fn3', { boolean: function () {} }) 54 | 55 | assert.throws(function () { fn('foo') }, /TypeError: Unexpected type of argument in function fn3 \(expected: boolean, actual: string, index: 0\)/) 56 | }) 57 | 58 | it('should give correct error in case of wrong type of argument (union args)', function () { 59 | const fn = typed({ 'boolean | string | Date': function () {} }) 60 | 61 | assert.throws(function () { fn(2) }, /TypeError: Unexpected type of argument in function unnamed \(expected: string or boolean or Date, actual: number, index: 0\)/) 62 | }) 63 | 64 | it('should give correct error in case of conflicting union arguments', function () { 65 | assert.throws(function () { 66 | typed({ 67 | 'string | number': function () {}, 68 | string: function () {} 69 | }) 70 | }, /TypeError: Conflicting signatures "string\|number" and "string"/) 71 | }) 72 | 73 | it('should give correct error in case of conflicting union arguments (2)', function () { 74 | assert.throws(function () { 75 | typed({ 76 | '...string | number': function () {}, 77 | '...string': function () {} 78 | }) 79 | }, /TypeError: Conflicting signatures "...string\|number" and "...string"/) 80 | }) 81 | 82 | it('should give correct error in case of conflicting rest params (1)', function () { 83 | assert.throws(function () { 84 | typed({ 85 | '...string': function () {}, 86 | string: function () {} 87 | }) 88 | }, /TypeError: Conflicting signatures "...string" and "string"/) 89 | }) 90 | 91 | it('should give correct error in case of conflicting rest params (2)', function () { 92 | // should not throw 93 | typed({ 94 | '...string': function () {}, 95 | 'string, number': function () {} 96 | }) 97 | 98 | assert.throws(function () { 99 | typed({ 100 | '...string': function () {}, 101 | 'string, string': function () {} 102 | }) 103 | }, /TypeError: Conflicting signatures "...string" and "string,string"/) 104 | }) 105 | 106 | it('should give correct error in case of conflicting rest params (3)', function () { 107 | assert.throws(function () { 108 | typed({ 109 | '...number|string': function () {}, 110 | 'number, string': function () {} 111 | }) 112 | }, /TypeError: Conflicting signatures "...number\|string" and "number,string"/) 113 | }) 114 | 115 | it('should give correct error in case of wrong type of argument (rest params)', function () { 116 | const fn = typed({ '...number': function () {} }) 117 | 118 | assert.throws(function () { fn(true) }, /TypeError: Unexpected type of argument in function unnamed \(expected: number, actual: boolean, index: 0\)/) 119 | assert.throws(function () { fn(2, true) }, /TypeError: Unexpected type of argument in function unnamed \(expected: number, actual: boolean, index: 1\)/) 120 | assert.throws(function () { fn(2, 3, true) }, /TypeError: Unexpected type of argument in function unnamed \(expected: number, actual: boolean, index: 2\)/) 121 | }) 122 | 123 | it('should give correct error in case of wrong type of argument (nested rest params)', function () { 124 | const fn = typed({ 'string, ...number': function () {} }) 125 | 126 | assert.throws(function () { fn(true) }, /TypeError: Unexpected type of argument in function unnamed \(expected: string, actual: boolean, index: 0\)/) 127 | assert.throws(function () { fn('foo', true) }, /TypeError: Unexpected type of argument in function unnamed \(expected: number, actual: boolean, index: 1\)/) 128 | assert.throws(function () { fn('foo', 2, true) }, /TypeError: Unexpected type of argument in function unnamed \(expected: number, actual: boolean, index: 2\)/) 129 | assert.throws(function () { fn('foo', 2, 3, true) }, /TypeError: Unexpected type of argument in function unnamed \(expected: number, actual: boolean, index: 3\)/) 130 | }) 131 | 132 | it('should give correct error in case of wrong type of argument (union and rest params)', function () { 133 | const fn = typed({ '...number|boolean': function () {} }) 134 | 135 | assert.throws(function () { fn('foo') }, /TypeError: Unexpected type of argument in function unnamed \(expected: number or boolean, actual: string, index: 0\)/) 136 | assert.throws(function () { fn(2, 'foo') }, /TypeError: Unexpected type of argument in function unnamed \(expected: number or boolean, actual: string, index: 1\)/) 137 | assert.throws(function () { fn(2, true, 'foo') }, /TypeError: Unexpected type of argument in function unnamed \(expected: number or boolean, actual: string, index: 2\)/) 138 | }) 139 | 140 | it('should only list matches of exact and convertable types', function () { 141 | const typed2 = typed.create() 142 | typed2.addConversion({ 143 | from: 'number', 144 | to: 'string', 145 | convert: function (x) { 146 | return +x 147 | } 148 | }) 149 | 150 | const fn1 = typed2({ string: function () {} }) 151 | const fn2 = typed2({ '...string': function () {} }) 152 | 153 | assert.throws(function () { fn1(true) }, /TypeError: Unexpected type of argument in function unnamed \(expected: string or number, actual: boolean, index: 0\)/) 154 | assert.throws(function () { fn2(true) }, /TypeError: Unexpected type of argument in function unnamed \(expected: string or number, actual: boolean, index: 0\)/) 155 | assert.throws(function () { fn2(2, true) }, /TypeError: Unexpected type of argument in function unnamed \(expected: string or number, actual: boolean, index: 1\)/) 156 | }) 157 | }) 158 | -------------------------------------------------------------------------------- /test/find.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import typed from '../src/typed-function.mjs' 3 | 4 | describe('find', function () { 5 | function a () {} 6 | function b () {} 7 | function c () {} 8 | function d () {} 9 | function e () {} 10 | 11 | const fn = typed('fn', { 12 | number: a, 13 | 'string, ...number': b, 14 | 'number, boolean': c, 15 | any: d, 16 | '': e 17 | }) 18 | 19 | const EXACT = { exact: true } 20 | 21 | it('should findSignature from an array with types', function () { 22 | assert.strictEqual(typed.findSignature(fn, ['number']).fn, a) 23 | assert.strictEqual(typed.findSignature(fn, ['number', 'boolean']).fn, c) 24 | assert.strictEqual(typed.findSignature(fn, ['any']).fn, d) 25 | assert.strictEqual(typed.findSignature(fn, []).fn, e) 26 | }) 27 | 28 | it('should find a signature from an array with types', function () { 29 | assert.strictEqual(typed.find(fn, ['number']), a) 30 | assert.strictEqual(typed.find(fn, ['number', 'boolean']), c) 31 | assert.strictEqual(typed.find(fn, ['any']), d) 32 | assert.strictEqual(typed.find(fn, []), e) 33 | }) 34 | 35 | it('should findSignature from a comma separated string with types', function () { 36 | assert.strictEqual(typed.findSignature(fn, 'number').fn, a) 37 | assert.strictEqual(typed.findSignature(fn, 'number,boolean').fn, c) 38 | assert.strictEqual(typed.findSignature(fn, ' number, boolean ').fn, c) // with spaces 39 | assert.strictEqual(typed.findSignature(fn, 'any').fn, d) 40 | assert.strictEqual(typed.findSignature(fn, '').fn, e) 41 | }) 42 | 43 | it('should find a signature from a comma separated string with types', function () { 44 | assert.strictEqual(typed.find(fn, 'number'), a) 45 | assert.strictEqual(typed.find(fn, 'number,boolean'), c) 46 | assert.strictEqual(typed.find(fn, ' number, boolean '), c) // with spaces 47 | assert.strictEqual(typed.find(fn, 'any'), d) 48 | assert.strictEqual(typed.find(fn, ''), e) 49 | }) 50 | 51 | it('should match rest params properly', function () { 52 | assert.strictEqual(typed.findSignature(fn, 'string, number').fn, b) 53 | assert.strictEqual(typed.findSignature(fn, 'string, number, number').fn, b) 54 | assert.strictEqual(typed.findSignature(fn, 'string, number, ...number').fn, b) 55 | }) 56 | 57 | it('should match any params properly', function () { 58 | assert.strictEqual(typed.find(fn, 'Array'), d) 59 | assert.throws( 60 | () => typed.find(fn, 'string, ...any'), 61 | /Signature not found/) 62 | const fn2 = typed({ '...any': e }) 63 | assert.strictEqual(typed.findSignature(fn2, '...number|string').fn, e) 64 | }) 65 | 66 | it('should throw an error when not found', function () { 67 | assert.throws(function () { 68 | typed.find(fn, 'number, number') 69 | }, /TypeError: Signature not found \(signature: fn\(number, number\)\)/) 70 | }) 71 | 72 | it('should handle non-exact matches as requested', function () { 73 | const t2 = typed.create() 74 | t2.addConversion({ 75 | from: 'number', 76 | to: 'string', 77 | convert: n => '' + n + ' much' 78 | }) 79 | const greeting = s => 'Hi ' + s 80 | const greet = t2('greet', { string: greeting }) 81 | const greetNumberSignature = t2.findSignature(greet, 'number') 82 | const greetNumber = t2.find(greet, 'number') 83 | assert.strictEqual(greetNumberSignature.fn, greeting) 84 | assert.strictEqual(greetNumber(42), 'Hi 42 much') 85 | assert.throws( 86 | () => t2.findSignature(greet, 'number', EXACT), 87 | /Signature not found/) 88 | assert.throws( 89 | () => t2.find(greet, 'number', EXACT), 90 | TypeError) 91 | assert.strictEqual(t2.find(greet, 'string'), greeting) 92 | }) 93 | 94 | it('should handle non-exact rest parameter matches', function () { 95 | const t2 = typed.create() 96 | t2.addConversion({ 97 | from: 'number', 98 | to: 'string', 99 | convert: n => '' + n + ' much' 100 | }) 101 | const greetAll = A => 'Hi ' + A.join(' and ') 102 | const greetRest = t2('greet', { '...string': greetAll }) 103 | const greetNumberSignature = t2.findSignature(greetRest, 'number') 104 | assert.strictEqual(greetNumberSignature.fn, greetAll) 105 | assert.strictEqual( 106 | greetNumberSignature.implementation.apply(null, [2]), 107 | 'Hi 2 much') 108 | assert.throws( 109 | () => t2.find(greetRest, 'number', EXACT), 110 | /Signature not found/) 111 | const greetSN = t2.findSignature(greetRest, 'string,number') 112 | assert.strictEqual(greetSN.fn, greetAll) 113 | assert.strictEqual( 114 | greetSN.implementation.apply(null, ['JJ', 2]), 115 | 'Hi JJ and 2 much') 116 | assert.throws( 117 | () => t2.find(greetRest, 'string,number', EXACT), 118 | /Signature not found/) 119 | const greetNRNS = t2.findSignature(greetRest, 'number,...number|string') 120 | assert.strictEqual(greetNRNS.fn, greetAll) 121 | assert.strictEqual( 122 | greetNRNS.implementation.apply(null, [0, 'JJ', 2]), 123 | 'Hi 0 much and JJ and 2 much') 124 | assert.throws( 125 | () => t2.find(greetRest, 'number,...number|string', EXACT), 126 | /Signature not found/) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /test/isTypedFunction.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import typed from '../src/typed-function.mjs' 3 | 4 | describe('isTypedFunction', function () { 5 | function a () {} 6 | function b () {} 7 | 8 | const fn = typed('fn', { 9 | number: a, 10 | string: b 11 | }) 12 | 13 | it('should distinguish typed functions from others', () => { 14 | assert.ok(typed.isTypedFunction(fn)) 15 | assert.strictEqual(typed.isTypedFunction(a), false) 16 | assert.strictEqual(typed.isTypedFunction(7), false) 17 | }) 18 | 19 | it('recognize typed functions from any typed instance', () => { 20 | const parallel = typed.create() 21 | const fn2 = parallel('fn', { 22 | number: b, 23 | string: a 24 | }) 25 | 26 | assert.ok(parallel.isTypedFunction(fn2)) 27 | assert.ok(parallel.isTypedFunction(fn)) 28 | assert.ok(typed.isTypedFunction(fn2)) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/merge.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import typed from '../src/typed-function.mjs' 3 | 4 | describe('merge', function () { 5 | it('should merge two typed-functions', function () { 6 | const typed1 = typed({ boolean: function (value) { return 'boolean:' + value } }) 7 | const typed2 = typed({ number: function (value) { return 'number:' + value } }) 8 | 9 | const typed3 = typed(typed1, typed2) 10 | 11 | assert.deepEqual(Object.keys(typed3.signatures).sort(), ['boolean', 'number']) 12 | 13 | assert.strictEqual(typed3(true), 'boolean:true') 14 | assert.strictEqual(typed3(2), 'number:2') 15 | assert.throws(function () { typed3('foo') }, /TypeError: Unexpected type of argument in function unnamed \(expected: number or boolean, actual: string, index: 0\)/) 16 | }) 17 | 18 | it('should merge three typed-functions', function () { 19 | const typed1 = typed({ boolean: function (value) { return 'boolean:' + value } }) 20 | const typed2 = typed({ number: function (value) { return 'number:' + value } }) 21 | const typed3 = typed({ string: function (value) { return 'string:' + value } }) 22 | 23 | const typed4 = typed(typed1, typed2, typed3) 24 | 25 | assert.deepEqual(Object.keys(typed4.signatures).sort(), ['boolean', 'number', 'string']) 26 | 27 | assert.strictEqual(typed4(true), 'boolean:true') 28 | assert.strictEqual(typed4(2), 'number:2') 29 | assert.strictEqual(typed4('foo'), 'string:foo') 30 | assert.throws(function () { typed4(new Date()) }, /TypeError: Unexpected type of argument in function unnamed \(expected: number or string or boolean, actual: Date, index: 0\)/) 31 | }) 32 | 33 | it('should merge two typed-functions with a custom name', function () { 34 | const typed1 = typed('typed1', { boolean: function (value) { return 'boolean:' + value } }) 35 | const typed2 = typed('typed2', { number: function (value) { return 'number:' + value } }) 36 | 37 | const typed3 = typed('typed3', typed1, typed2) 38 | 39 | assert.equal(typed3.name, 'typed3') 40 | }) 41 | 42 | it('should merge a typed function with an object of signatures', () => { 43 | const typed1 = typed({ boolean: b => !b, string: s => '!' + s }) 44 | const typed2 = typed(typed1, { number: n => 1 - n }) 45 | 46 | assert.equal(typed2(true), false) 47 | assert.equal(typed2('true'), '!true') 48 | assert.equal(typed2(1), 0) 49 | }) 50 | 51 | it('should merge two objects of signatures', () => { 52 | const typed1 = typed( 53 | { boolean: b => !b, string: s => '!' + s }, 54 | { number: n => 1 - n } 55 | ) 56 | 57 | assert.equal(typed1(true), false) 58 | assert.equal(typed1('true'), '!true') 59 | assert.equal(typed1(1), 0) 60 | }) 61 | 62 | it('should not copy conversions as exact signatures', function () { 63 | const typed2 = typed.create() 64 | typed2.addConversion( 65 | { from: 'string', to: 'number', convert: function (x) { return parseFloat(x) } } 66 | ) 67 | 68 | const fn2 = typed2({ number: function (value) { return value } }) 69 | 70 | assert.strictEqual(fn2(2), 2) 71 | assert.strictEqual(fn2('123'), 123) 72 | 73 | const fn1 = typed({ Date: function (value) { return value } }) 74 | const fn3 = typed(fn1, fn2) // create via typed which has no conversions 75 | 76 | const date = new Date() 77 | assert.strictEqual(fn3(2), 2) 78 | assert.strictEqual(fn3(date), date) 79 | assert.throws(function () { fn3('123') }, /TypeError: Unexpected type of argument in function unnamed \(expected: number or Date, actual: string, index: 0\)/) 80 | }) 81 | 82 | it('should allow merging duplicate signatures when pointing to the same function', function () { 83 | const typed1 = typed({ boolean: function (value) { return 'boolean:' + value } }) 84 | 85 | const merged = typed(typed1, typed1) 86 | 87 | assert.deepEqual(Object.keys(merged.signatures).sort(), ['boolean']) 88 | }) 89 | 90 | it('should throw an error in case of conflicting signatures when merging', function () { 91 | const typed1 = typed({ boolean: function (value) { return 'boolean:' + value } }) 92 | const typed2 = typed({ boolean: function (value) { return 'boolean:' + value } }) 93 | 94 | assert.throws(function () { 95 | typed(typed1, typed2) 96 | }, /Error: Signature "boolean" is defined twice/) 97 | }) 98 | 99 | it('should throw an error in case of conflicting names when merging', function () { 100 | const typed1 = typed('fn1', { boolean: function () {} }) 101 | const typed2 = typed('fn2', { string: function () {} }) 102 | const typed3 = typed({ number: function () {} }) 103 | 104 | assert.throws(function () { 105 | typed(typed1, typed2) 106 | }, /Error: Function names do not match \(expected: fn1, actual: fn2\)/) 107 | 108 | const typed4 = typed(typed2, typed3) 109 | assert.equal(typed4.name, 'fn2') 110 | }) 111 | 112 | it('should be able to use referTo when merging signatures from multiple typed-functions', function () { 113 | function add1 (a, b) { 114 | return 'add1:' + (a + b) 115 | } 116 | 117 | function add2 (a, b) { 118 | return 'add2:' + (a + b) 119 | } 120 | 121 | const fn1 = typed({ 122 | 'number,number': add1, 123 | string: typed.referTo('number,number', (fnNumberNumber) => { 124 | return function (valuesString) { 125 | const values = valuesString.split(',').map(Number) 126 | return fnNumberNumber.apply(null, values) 127 | } 128 | }) 129 | }) 130 | 131 | const fn2 = typed({ 132 | 'number,number': add2 133 | }) 134 | 135 | assert.equal(fn1('2,3'), 'add1:5') 136 | assert.equal(fn2(2, 3), 'add2:5') 137 | 138 | const fn3 = typed({ 139 | ...fn1.signatures, 140 | ...fn2.signatures // <-- will override the 'number,number' signature of fn1 with the one of fn2 141 | }) 142 | 143 | assert.equal(fn3('2,3'), 'add2:5') 144 | }) 145 | 146 | it('should be able to use referToSelf across merged signatures', function () { 147 | const fn1 = typed({ 148 | '...number': function (values) { 149 | let sum = 0 150 | for (let i = 0; i < values.length; i++) { 151 | sum += values[i] 152 | } 153 | return sum 154 | } 155 | }) 156 | 157 | const fn2 = typed({ 158 | '...string': typed.referToSelf((self) => { 159 | return function (values) { 160 | assert.strictEqual(self, fn3) // only holds after merging fn1 and fn2 161 | 162 | const newValues = [] 163 | for (let i = 0; i < values.length; i++) { 164 | newValues[i] = parseInt(values[i], 10) 165 | } 166 | return self.apply(null, newValues) 167 | } 168 | }) 169 | }) 170 | 171 | const fn3 = typed(fn1, fn2) 172 | 173 | assert.equal(fn3('1', '2', '3'), '6') 174 | assert.equal(fn3(1, 2, 3), 6) 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /test/onMismatch.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import typed from '../src/typed-function.mjs' 3 | 4 | describe('onMismatch handler', () => { 5 | const square = typed('square', { 6 | number: x => x * x, 7 | string: s => s + s 8 | }) 9 | 10 | it('should replace the return value of a mismatched call', () => { 11 | typed.onMismatch = () => 42 12 | assert.strictEqual(square(5), 25) 13 | assert.strictEqual(square('yo'), 'yoyo') 14 | assert.strictEqual(square([13]), 42) 15 | }) 16 | 17 | const myErrorLog = [] 18 | it('should allow error logging', () => { 19 | typed.onMismatch = (name, args, signatures) => { 20 | myErrorLog.push(typed.createError(name, args, signatures)) 21 | return null 22 | } 23 | square({ the: 'circle' }) 24 | square(7) 25 | square('me') 26 | square(1, 2) 27 | assert.strictEqual(myErrorLog.length, 2) 28 | assert('data' in myErrorLog[0]) 29 | }) 30 | 31 | it('should allow changing the error', () => { 32 | typed.onMismatch = name => { throw Error('Problem with ' + name) } 33 | assert.throws(() => square(['one']), /Problem with square/) 34 | }) 35 | 36 | it('should allow a return to standard behavior', () => { 37 | typed.onMismatch = typed.throwMismatchError 38 | assert.throws(() => square('be', 'there'), TypeError) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/resolve.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import typed from '../src/typed-function.mjs' 3 | 4 | describe('resolve', function () { 5 | before(() => typed.addConversion({ 6 | from: 'boolean', to: 'string', convert: x => '' + x 7 | })) 8 | 9 | after(() => { typed.clearConversions() }) 10 | 11 | it('should choose the signature that direct execution would', () => { 12 | const fn = typed({ 13 | number: n => 'b ' + n, 14 | boolean: b => b ? 'c' : 'd', 15 | 'number, string': (n, s) => 'e ' + n + ' ' + s, 16 | '...string': a => 'f ' + a.length, 17 | '...': a => 'g ' + a.length 18 | }) 19 | const examples = [ 20 | [3], 21 | ['hello'], 22 | [false], 23 | [3, 'me'], 24 | [0, true], 25 | ['x', 'y', 'z'], 26 | [false, 'y', false], 27 | [[1]], 28 | ['x', [1], 'z', 'w'] 29 | ] 30 | for (const example of examples) { 31 | assert.strictEqual( 32 | typed.resolve(fn, example).implementation.apply(null, example), 33 | fn.apply(fn, example) 34 | ) 35 | } 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /test/rest_params.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import typed from '../src/typed-function.mjs' 3 | import { strictEqualArray } from './strictEqualArray.mjs' 4 | 5 | describe('rest parameters', function () { 6 | it('should create a typed function with rest parameters', function () { 7 | const sum = typed({ 8 | '...number': function (values) { 9 | assert(Array.isArray(values)) 10 | let sum = 0 11 | for (let i = 0; i < values.length; i++) { 12 | sum += values[i] 13 | } 14 | return sum 15 | } 16 | }) 17 | 18 | assert.equal(sum(2), 2) 19 | assert.equal(sum(2, 3, 4), 9) 20 | assert.throws(function () { sum() }, /TypeError: Too few arguments in function unnamed \(expected: number, index: 0\)/) 21 | assert.throws(function () { sum(true) }, /TypeError: Unexpected type of argument in function unnamed \(expected: number, actual: boolean, index: 0\)/) 22 | assert.throws(function () { sum('string') }, /TypeError: Unexpected type of argument in function unnamed \(expected: number, actual: string, index: 0\)/) 23 | assert.throws(function () { sum(2, 'string') }, /TypeError: Unexpected type of argument in function unnamed \(expected: number, actual: string, index: 1\)/) 24 | assert.throws(function () { sum(2, 3, 'string') }, /TypeError: Unexpected type of argument in function unnamed \(expected: number, actual: string, index: 2\)/) 25 | }) 26 | 27 | it('should create a typed function with rest parameters (2)', function () { 28 | const fn = typed({ 29 | 'string, ...number': function (str, values) { 30 | assert.equal(typeof str, 'string') 31 | assert(Array.isArray(values)) 32 | return str + ': ' + values.join(', ') 33 | } 34 | }) 35 | 36 | assert.equal(fn('foo', 2), 'foo: 2') 37 | assert.equal(fn('foo', 2, 4), 'foo: 2, 4') 38 | assert.throws(function () { fn(2, 4) }, /TypeError: Unexpected type of argument in function unnamed \(expected: string, actual: number, index: 0\)/) 39 | assert.throws(function () { fn('string') }, /TypeError: Too few arguments in function unnamed \(expected: number, index: 1\)/) 40 | assert.throws(function () { fn('string', 'string') }, /TypeError: Unexpected type of argument in function unnamed \(expected: number, actual: string, index: 1\)/) 41 | }) 42 | 43 | it('should create a typed function with any type arguments (1)', function () { 44 | const fn = typed({ 45 | 'string, ...any': function (str, values) { 46 | assert.equal(typeof str, 'string') 47 | assert(Array.isArray(values)) 48 | return str + ': ' + values.join(', ') 49 | } 50 | }) 51 | 52 | assert.equal(fn('foo', 2), 'foo: 2') 53 | assert.equal(fn('foo', 2, true, 'bar'), 'foo: 2, true, bar') 54 | assert.equal(fn('foo', 'bar'), 'foo: bar') 55 | assert.throws(function () { fn(2, 4) }, /TypeError: Unexpected type of argument in function unnamed \(expected: string, actual: number, index: 0\)/) 56 | assert.throws(function () { fn('string') }, /TypeError: Too few arguments in function unnamed \(expected: any, index: 1\)/) 57 | }) 58 | 59 | it('should create a typed function with implicit any type arguments', function () { 60 | const fn = typed({ 61 | 'string, ...': function (str, values) { 62 | assert.equal(typeof str, 'string') 63 | assert(Array.isArray(values)) 64 | return str + ': ' + values.join(', ') 65 | } 66 | }) 67 | 68 | assert.equal(fn('foo', 2), 'foo: 2') 69 | assert.equal(fn('foo', 2, true, 'bar'), 'foo: 2, true, bar') 70 | assert.equal(fn('foo', 'bar'), 'foo: bar') 71 | assert.throws(function () { fn(2, 4) }, /TypeError: Unexpected type of argument in function unnamed \(expected: string, actual: number, index: 0\)/) 72 | assert.throws(function () { fn('string') }, /TypeError: Too few arguments in function unnamed \(expected: any, index: 1\)/) 73 | }) 74 | 75 | it('should create a typed function with any type arguments (2)', function () { 76 | const fn = typed({ 77 | 'any, ...number': function (any, values) { 78 | assert(Array.isArray(values)) 79 | return any + ': ' + values.join(', ') 80 | } 81 | }) 82 | 83 | assert.equal(fn('foo', 2), 'foo: 2') 84 | assert.equal(fn(1, 2, 4), '1: 2, 4') 85 | assert.equal(fn(null, 2, 4), 'null: 2, 4') 86 | assert.throws(function () { fn('string') }, /TypeError: Too few arguments in function unnamed \(expected: number, index: 1\)/) 87 | assert.throws(function () { fn('string', 'string') }, /TypeError: Unexpected type of argument in function unnamed \(expected: number, actual: string, index: 1\)/) 88 | }) 89 | 90 | it('should create a typed function with union type arguments', function () { 91 | const fn = typed({ 92 | '...number|string': function (values) { 93 | assert(Array.isArray(values)) 94 | return values 95 | } 96 | }) 97 | 98 | strictEqualArray(fn(2, 3, 4), [2, 3, 4]) 99 | strictEqualArray(fn('a', 'b', 'c'), ['a', 'b', 'c']) 100 | strictEqualArray(fn('a', 2, 'c', 3), ['a', 2, 'c', 3]) 101 | assert.throws(function () { fn() }, /TypeError: Too few arguments in function unnamed \(expected: number or string, index: 0\)/) 102 | assert.throws(function () { fn('string', true) }, /TypeError: Unexpected type of argument. Index: 1 in function unnamed \(expected: string | number/) 103 | assert.throws(function () { fn(2, false) }, /TypeError: Unexpected type of argument. Index: 1 in function unnamed \(expected: string | number/) 104 | assert.throws(function () { fn(2, 3, false) }, /TypeError: Unexpected type of argument. Index: 2 in function unnamed \(expected: string | number/) 105 | }) 106 | 107 | it('should create a composed function with rest parameters', function () { 108 | const fn = typed({ 109 | 'string, ...number': function (str, values) { 110 | assert.equal(typeof str, 'string') 111 | assert(Array.isArray(values)) 112 | return str + ': ' + values.join(', ') 113 | }, 114 | 115 | '...boolean': function (values) { 116 | assert(Array.isArray(values)) 117 | return 'booleans' 118 | } 119 | }) 120 | 121 | assert.equal(fn('foo', 2), 'foo: 2') 122 | assert.equal(fn('foo', 2, 4), 'foo: 2, 4') 123 | assert.equal(fn(true, false, false), 'booleans') 124 | assert.throws(function () { fn(2, 4) }, /TypeError: Unexpected type of argument in function unnamed \(expected: string or boolean, actual: number, index: 0\)/) 125 | assert.throws(function () { fn('string') }, /TypeError: Too few arguments in function unnamed \(expected: number, index: 1\)/) 126 | assert.throws(function () { fn('string', true) }, /TypeError: Unexpected type of argument in function unnamed \(expected: number, actual: boolean, index: 1\)/) 127 | }) 128 | 129 | it('should continue with other options if rest params do not match', function () { 130 | const fn = typed({ 131 | '...number': function (values) { 132 | return '...number' 133 | }, 134 | 135 | Object: function (value) { 136 | return 'Object' 137 | } 138 | }) 139 | 140 | assert.equal(fn(2, 3), '...number') 141 | assert.equal(fn(2), '...number') 142 | assert.equal(fn({}), 'Object') 143 | 144 | assert.equal(Object.keys(fn.signatures).length, 2) 145 | assert.ok('Object' in fn.signatures) 146 | assert.ok('...number' in fn.signatures) 147 | }) 148 | 149 | it('should split rest params with conversions in two and order them correctly', function () { 150 | const typed2 = typed.create() 151 | typed2.addConversion( 152 | { from: 'string', to: 'number', convert: function (x) { return parseFloat(x) } } 153 | ) 154 | 155 | const fn = typed2({ 156 | '...number': function (values) { 157 | return values 158 | }, 159 | 160 | '...string': function (value) { 161 | return value 162 | } 163 | }) 164 | 165 | assert.deepEqual(fn(2, 3), [2, 3]) 166 | assert.deepEqual(fn(2), [2]) 167 | assert.deepEqual(fn(2, '4'), [2, 4]) 168 | assert.deepEqual(fn('2', 4), [2, 4]) 169 | assert.deepEqual(fn('foo'), ['foo']) 170 | assert.deepEqual(Object.keys(fn.signatures), [ 171 | '...number', 172 | '...string' 173 | ]) 174 | }) 175 | 176 | it('should throw an error in case of unexpected rest parameters', function () { 177 | assert.throws(function () { 178 | typed({ '...number, string': function () {} }) 179 | }, /SyntaxError: Unexpected rest parameter "...number": only allowed for the last parameter/) 180 | }) 181 | 182 | it('should correctly interact with any', function () { 183 | const fn = typed({ 184 | string: function () { 185 | return 'one' 186 | }, 187 | '...any': function () { 188 | return 'two' 189 | } 190 | }) 191 | 192 | assert.equal(fn('a'), 'one') 193 | assert.equal(fn([]), 'two') 194 | assert.equal(fn('a', 'a'), 'two') 195 | assert.equal(fn('a', []), 'two') 196 | assert.equal(fn([], []), 'two') 197 | }) 198 | }) 199 | -------------------------------------------------------------------------------- /test/security.test.mjs: -------------------------------------------------------------------------------- 1 | import typed from '../src/typed-function.mjs' 2 | 3 | describe('security', function () { 4 | it('should not allow bad code in the function name', function () { 5 | // simple example: 6 | // var fn = typed("(){}+console.log('hacked...');function a", { 7 | // "": function () {} 8 | // }); 9 | 10 | // example resulting in throwing an error if successful 11 | typed("(){}+(function(){throw new Error('Hacked... should not have executed this function!!!')})();function a", { 12 | '': function () {} 13 | }) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /test/strictEqualArray.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | 3 | /** 4 | * Test strict equality of all elements in two arrays. 5 | * @param {Array} a 6 | * @param {Array} b 7 | */ 8 | export function strictEqualArray (a, b) { 9 | assert.strictEqual(a.length, b.length) 10 | 11 | for (let i = 0; i < a.length; a++) { 12 | assert.strictEqual(a[i], b[i]) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/union_types.test.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import typed from '../src/typed-function.mjs' 3 | 4 | describe('union types', function () { 5 | it('should create a typed function with union types', function () { 6 | const fn = typed({ 7 | 'number | boolean': function (arg) { 8 | return typeof arg 9 | } 10 | }) 11 | 12 | assert.equal(fn(true), 'boolean') 13 | assert.equal(fn(2), 'number') 14 | assert.throws(function () { fn('string') }, /TypeError: Unexpected type of argument in function unnamed \(expected: number or boolean, actual: string, index: 0\)/) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /tools/cjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | --------------------------------------------------------------------------------