├── .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 | [](https://www.npmjs.com/package/typed-function)
4 | [](https://www.npmjs.com/package/typed-function)
5 | [](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 |
--------------------------------------------------------------------------------