simple-squiggle

A restricted subset of Squiggle
Log | Files | Refs | README

typed-function.js (43896B)


      1 /**
      2  * typed-function
      3  *
      4  * Type checking for JavaScript functions
      5  *
      6  * https://github.com/josdejong/typed-function
      7  */
      8 'use strict';
      9 
     10 (function (root, factory) {
     11   if (typeof define === 'function' && define.amd) {
     12     // AMD. Register as an anonymous module.
     13     define([], factory);
     14   } else if (typeof exports === 'object') {
     15     // OldNode. Does not work with strict CommonJS, but
     16     // only CommonJS-like environments that support module.exports,
     17     // like OldNode.
     18     module.exports = factory();
     19   } else {
     20     // Browser globals (root is window)
     21     root.typed = factory();
     22   }
     23 }(this, function () {
     24 
     25   function ok () {
     26     return true;
     27   }
     28 
     29   function notOk () {
     30     return false;
     31   }
     32 
     33   function undef () {
     34     return undefined;
     35   }
     36 
     37   /**
     38    * @typedef {{
     39    *   params: Param[],
     40    *   fn: function
     41    * }} Signature
     42    *
     43    * @typedef {{
     44    *   types: Type[],
     45    *   restParam: boolean
     46    * }} Param
     47    *
     48    * @typedef {{
     49    *   name: string,
     50    *   typeIndex: number,
     51    *   test: function,
     52    *   conversion?: ConversionDef,
     53    *   conversionIndex: number,
     54    * }} Type
     55    *
     56    * @typedef {{
     57    *   from: string,
     58    *   to: string,
     59    *   convert: function (*) : *
     60    * }} ConversionDef
     61    *
     62    * @typedef {{
     63    *   name: string,
     64    *   test: function(*) : boolean
     65    * }} TypeDef
     66    */
     67 
     68   // create a new instance of typed-function
     69   function create () {
     70     // data type tests
     71     var _types = [
     72       { name: 'number',    test: function (x) { return typeof x === 'number' } },
     73       { name: 'string',    test: function (x) { return typeof x === 'string' } },
     74       { name: 'boolean',   test: function (x) { return typeof x === 'boolean' } },
     75       { name: 'Function',  test: function (x) { return typeof x === 'function'} },
     76       { name: 'Array',     test: Array.isArray },
     77       { name: 'Date',      test: function (x) { return x instanceof Date } },
     78       { name: 'RegExp',    test: function (x) { return x instanceof RegExp } },
     79       { name: 'Object',    test: function (x) {
     80         return typeof x === 'object' && x !== null && x.constructor === Object
     81       }},
     82       { name: 'null',      test: function (x) { return x === null } },
     83       { name: 'undefined', test: function (x) { return x === undefined } }
     84     ];
     85 
     86     var anyType = {
     87       name: 'any',
     88       test: ok
     89     }
     90 
     91     // types which need to be ignored
     92     var _ignore = [];
     93 
     94     // type conversions
     95     var _conversions = [];
     96 
     97     // This is a temporary object, will be replaced with a typed function at the end
     98     var typed = {
     99       types: _types,
    100       conversions: _conversions,
    101       ignore: _ignore
    102     };
    103 
    104     /**
    105      * Find the test function for a type
    106      * @param {String} typeName
    107      * @return {TypeDef} Returns the type definition when found,
    108      *                    Throws a TypeError otherwise
    109      */
    110     function findTypeByName (typeName) {
    111       var entry = findInArray(typed.types, function (entry) {
    112         return entry.name === typeName;
    113       });
    114 
    115       if (entry) {
    116         return entry;
    117       }
    118 
    119       if (typeName === 'any') { // special baked-in case 'any'
    120         return anyType;
    121       }
    122 
    123       var hint = findInArray(typed.types, function (entry) {
    124         return entry.name.toLowerCase() === typeName.toLowerCase();
    125       });
    126 
    127       throw new TypeError('Unknown type "' + typeName + '"' +
    128           (hint ? ('. Did you mean "' + hint.name + '"?') : ''));
    129     }
    130 
    131     /**
    132      * Find the index of a type definition. Handles special case 'any'
    133      * @param {TypeDef} type
    134      * @return {number}
    135      */
    136     function findTypeIndex(type) {
    137       if (type === anyType) {
    138         return 999;
    139       }
    140 
    141       return typed.types.indexOf(type);
    142     }
    143 
    144     /**
    145      * Find a type that matches a value.
    146      * @param {*} value
    147      * @return {string} Returns the name of the first type for which
    148      *                  the type test matches the value.
    149      */
    150     function findTypeName(value) {
    151       var entry = findInArray(typed.types, function (entry) {
    152         return entry.test(value);
    153       });
    154 
    155       if (entry) {
    156         return entry.name;
    157       }
    158 
    159       throw new TypeError('Value has unknown type. Value: ' + value);
    160     }
    161 
    162     /**
    163      * Find a specific signature from a (composed) typed function, for example:
    164      *
    165      *   typed.find(fn, ['number', 'string'])
    166      *   typed.find(fn, 'number, string')
    167      *
    168      * Function find only only works for exact matches.
    169      *
    170      * @param {Function} fn                   A typed-function
    171      * @param {string | string[]} signature   Signature to be found, can be
    172      *                                        an array or a comma separated string.
    173      * @return {Function}                     Returns the matching signature, or
    174      *                                        throws an error when no signature
    175      *                                        is found.
    176      */
    177     function find (fn, signature) {
    178       if (!fn.signatures) {
    179         throw new TypeError('Function is no typed-function');
    180       }
    181 
    182       // normalize input
    183       var arr;
    184       if (typeof signature === 'string') {
    185         arr = signature.split(',');
    186         for (var i = 0; i < arr.length; i++) {
    187           arr[i] = arr[i].trim();
    188         }
    189       }
    190       else if (Array.isArray(signature)) {
    191         arr = signature;
    192       }
    193       else {
    194         throw new TypeError('String array or a comma separated string expected');
    195       }
    196 
    197       var str = arr.join(',');
    198 
    199       // find an exact match
    200       var match = fn.signatures[str];
    201       if (match) {
    202         return match;
    203       }
    204 
    205       // TODO: extend find to match non-exact signatures
    206 
    207       throw new TypeError('Signature not found (signature: ' + (fn.name || 'unnamed') + '(' + arr.join(', ') + '))');
    208     }
    209 
    210     /**
    211      * Convert a given value to another data type.
    212      * @param {*} value
    213      * @param {string} type
    214      */
    215     function convert (value, type) {
    216       var from = findTypeName(value);
    217 
    218       // check conversion is needed
    219       if (type === from) {
    220         return value;
    221       }
    222 
    223       for (var i = 0; i < typed.conversions.length; i++) {
    224         var conversion = typed.conversions[i];
    225         if (conversion.from === from && conversion.to === type) {
    226           return conversion.convert(value);
    227         }
    228       }
    229 
    230       throw new Error('Cannot convert from ' + from + ' to ' + type);
    231     }
    232     
    233     /**
    234      * Stringify parameters in a normalized way
    235      * @param {Param[]} params
    236      * @return {string}
    237      */
    238     function stringifyParams (params) {
    239       return params
    240           .map(function (param) {
    241             var typeNames = param.types.map(getTypeName);
    242 
    243             return (param.restParam ? '...' : '') + typeNames.join('|');
    244           })
    245           .join(',');
    246     }
    247 
    248     /**
    249      * Parse a parameter, like "...number | boolean"
    250      * @param {string} param
    251      * @param {ConversionDef[]} conversions
    252      * @return {Param} param
    253      */
    254     function parseParam (param, conversions) {
    255       var restParam = param.indexOf('...') === 0;
    256       var types = (!restParam)
    257           ? param
    258           : (param.length > 3)
    259               ? param.slice(3)
    260               : 'any';
    261 
    262       var typeNames = types.split('|').map(trim)
    263           .filter(notEmpty)
    264           .filter(notIgnore);
    265 
    266       var matchingConversions = filterConversions(conversions, typeNames);
    267 
    268       var exactTypes = typeNames.map(function (typeName) {
    269         var type = findTypeByName(typeName);
    270 
    271         return {
    272           name: typeName,
    273           typeIndex: findTypeIndex(type),
    274           test: type.test,
    275           conversion: null,
    276           conversionIndex: -1
    277         };
    278       });
    279 
    280       var convertibleTypes = matchingConversions.map(function (conversion) {
    281         var type = findTypeByName(conversion.from);
    282 
    283         return {
    284           name: conversion.from,
    285           typeIndex: findTypeIndex(type),
    286           test: type.test,
    287           conversion: conversion,
    288           conversionIndex: conversions.indexOf(conversion)
    289         };
    290       });
    291 
    292       return {
    293         types: exactTypes.concat(convertibleTypes),
    294         restParam: restParam
    295       };
    296     }
    297 
    298     /**
    299      * Parse a signature with comma separated parameters,
    300      * like "number | boolean, ...string"
    301      * @param {string} signature
    302      * @param {function} fn
    303      * @param {ConversionDef[]} conversions
    304      * @return {Signature | null} signature
    305      */
    306     function parseSignature (signature, fn, conversions) {
    307       var params = [];
    308 
    309       if (signature.trim() !== '') {
    310         params = signature
    311             .split(',')
    312             .map(trim)
    313             .map(function (param, index, array) {
    314               var parsedParam = parseParam(param, conversions);
    315 
    316               if (parsedParam.restParam && (index !== array.length - 1)) {
    317                 throw new SyntaxError('Unexpected rest parameter "' + param + '": ' +
    318                     'only allowed for the last parameter');
    319               }
    320 
    321               return parsedParam;
    322           });
    323       }
    324 
    325       if (params.some(isInvalidParam)) {
    326         // invalid signature: at least one parameter has no types
    327         // (they may have been filtered)
    328         return null;
    329       }
    330 
    331       return {
    332         params: params,
    333         fn: fn
    334       };
    335     }
    336 
    337     /**
    338      * Test whether a set of params contains a restParam
    339      * @param {Param[]} params
    340      * @return {boolean} Returns true when the last parameter is a restParam
    341      */
    342     function hasRestParam(params) {
    343       var param = last(params)
    344       return param ? param.restParam : false;
    345     }
    346 
    347     /**
    348      * Test whether a parameter contains conversions
    349      * @param {Param} param
    350      * @return {boolean} Returns true when at least one of the parameters
    351      *                   contains a conversion.
    352      */
    353     function hasConversions(param) {
    354       return param.types.some(function (type) {
    355         return type.conversion != null;
    356       });
    357     }
    358 
    359     /**
    360      * Create a type test for a single parameter, which can have one or multiple
    361      * types.
    362      * @param {Param} param
    363      * @return {function(x: *) : boolean} Returns a test function
    364      */
    365     function compileTest(param) {
    366       if (!param || param.types.length === 0) {
    367         // nothing to do
    368         return ok;
    369       }
    370       else if (param.types.length === 1) {
    371         return findTypeByName(param.types[0].name).test;
    372       }
    373       else if (param.types.length === 2) {
    374         var test0 = findTypeByName(param.types[0].name).test;
    375         var test1 = findTypeByName(param.types[1].name).test;
    376         return function or(x) {
    377           return test0(x) || test1(x);
    378         }
    379       }
    380       else { // param.types.length > 2
    381         var tests = param.types.map(function (type) {
    382           return findTypeByName(type.name).test;
    383         })
    384         return function or(x) {
    385           for (var i = 0; i < tests.length; i++) {
    386             if (tests[i](x)) {
    387               return true;
    388             }
    389           }
    390           return false;
    391         }
    392       }
    393     }
    394 
    395     /**
    396      * Create a test for all parameters of a signature
    397      * @param {Param[]} params
    398      * @return {function(args: Array<*>) : boolean}
    399      */
    400     function compileTests(params) {
    401       var tests, test0, test1;
    402 
    403       if (hasRestParam(params)) {
    404         // variable arguments like '...number'
    405         tests = initial(params).map(compileTest);
    406         var varIndex = tests.length;
    407         var lastTest = compileTest(last(params));
    408         var testRestParam = function (args) {
    409           for (var i = varIndex; i < args.length; i++) {
    410             if (!lastTest(args[i])) {
    411               return false;
    412             }
    413           }
    414           return true;
    415         }
    416 
    417         return function testArgs(args) {
    418           for (var i = 0; i < tests.length; i++) {
    419             if (!tests[i](args[i])) {
    420               return false;
    421             }
    422           }
    423           return testRestParam(args) && (args.length >= varIndex + 1);
    424         };
    425       }
    426       else {
    427         // no variable arguments
    428         if (params.length === 0) {
    429           return function testArgs(args) {
    430             return args.length === 0;
    431           };
    432         }
    433         else if (params.length === 1) {
    434           test0 = compileTest(params[0]);
    435           return function testArgs(args) {
    436             return test0(args[0]) && args.length === 1;
    437           };
    438         }
    439         else if (params.length === 2) {
    440           test0 = compileTest(params[0]);
    441           test1 = compileTest(params[1]);
    442           return function testArgs(args) {
    443             return test0(args[0]) && test1(args[1]) && args.length === 2;
    444           };
    445         }
    446         else { // arguments.length > 2
    447           tests = params.map(compileTest);
    448           return function testArgs(args) {
    449             for (var i = 0; i < tests.length; i++) {
    450               if (!tests[i](args[i])) {
    451                 return false;
    452               }
    453             }
    454             return args.length === tests.length;
    455           };
    456         }
    457       }
    458     }
    459 
    460     /**
    461      * Find the parameter at a specific index of a signature.
    462      * Handles rest parameters.
    463      * @param {Signature} signature
    464      * @param {number} index
    465      * @return {Param | null} Returns the matching parameter when found,
    466      *                        null otherwise.
    467      */
    468     function getParamAtIndex(signature, index) {
    469       return index < signature.params.length
    470           ? signature.params[index]
    471           : hasRestParam(signature.params)
    472               ? last(signature.params)
    473               : null
    474     }
    475 
    476     /**
    477      * Get all type names of a parameter
    478      * @param {Signature} signature
    479      * @param {number} index
    480      * @param {boolean} excludeConversions
    481      * @return {string[]} Returns an array with type names
    482      */
    483     function getExpectedTypeNames (signature, index, excludeConversions) {
    484       var param = getParamAtIndex(signature, index);
    485       var types = param
    486           ? excludeConversions
    487                   ? param.types.filter(isExactType)
    488                   : param.types
    489           : [];
    490 
    491       return types.map(getTypeName);
    492     }
    493 
    494     /**
    495      * Returns the name of a type
    496      * @param {Type} type
    497      * @return {string} Returns the type name
    498      */
    499     function getTypeName(type) {
    500       return type.name;
    501     }
    502 
    503     /**
    504      * Test whether a type is an exact type or conversion
    505      * @param {Type} type
    506      * @return {boolean} Returns true when
    507      */
    508     function isExactType(type) {
    509       return type.conversion === null || type.conversion === undefined;
    510     }
    511 
    512     /**
    513      * Helper function for creating error messages: create an array with
    514      * all available types on a specific argument index.
    515      * @param {Signature[]} signatures
    516      * @param {number} index
    517      * @return {string[]} Returns an array with available types
    518      */
    519     function mergeExpectedParams(signatures, index) {
    520       var typeNames = uniq(flatMap(signatures, function (signature) {
    521         return getExpectedTypeNames(signature, index, false);
    522       }));
    523 
    524       return (typeNames.indexOf('any') !== -1) ? ['any'] : typeNames;
    525     }
    526 
    527     /**
    528      * Create
    529      * @param {string} name             The name of the function
    530      * @param {array.<*>} args          The actual arguments passed to the function
    531      * @param {Signature[]} signatures  A list with available signatures
    532      * @return {TypeError} Returns a type error with additional data
    533      *                     attached to it in the property `data`
    534      */
    535     function createError(name, args, signatures) {
    536       var err, expected;
    537       var _name = name || 'unnamed';
    538 
    539       // test for wrong type at some index
    540       var matchingSignatures = signatures;
    541       var index;
    542       for (index = 0; index < args.length; index++) {
    543         var nextMatchingDefs = matchingSignatures.filter(function (signature) {
    544           var test = compileTest(getParamAtIndex(signature, index));
    545           return (index < signature.params.length || hasRestParam(signature.params)) &&
    546               test(args[index]);
    547         });
    548 
    549         if (nextMatchingDefs.length === 0) {
    550           // no matching signatures anymore, throw error "wrong type"
    551           expected = mergeExpectedParams(matchingSignatures, index);
    552           if (expected.length > 0) {
    553             var actualType = findTypeName(args[index]);
    554 
    555             err = new TypeError('Unexpected type of argument in function ' + _name +
    556                 ' (expected: ' + expected.join(' or ') +
    557                 ', actual: ' + actualType + ', index: ' + index + ')');
    558             err.data = {
    559               category: 'wrongType',
    560               fn: _name,
    561               index: index,
    562               actual: actualType,
    563               expected: expected
    564             }
    565             return err;
    566           }
    567         }
    568         else {
    569           matchingSignatures = nextMatchingDefs;
    570         }
    571       }
    572 
    573       // test for too few arguments
    574       var lengths = matchingSignatures.map(function (signature) {
    575         return hasRestParam(signature.params) ? Infinity : signature.params.length;
    576       });
    577       if (args.length < Math.min.apply(null, lengths)) {
    578         expected = mergeExpectedParams(matchingSignatures, index);
    579         err = new TypeError('Too few arguments in function ' + _name +
    580             ' (expected: ' + expected.join(' or ') +
    581             ', index: ' + args.length + ')');
    582         err.data = {
    583           category: 'tooFewArgs',
    584           fn: _name,
    585           index: args.length,
    586           expected: expected
    587         }
    588         return err;
    589       }
    590 
    591       // test for too many arguments
    592       var maxLength = Math.max.apply(null, lengths);
    593       if (args.length > maxLength) {
    594         err = new TypeError('Too many arguments in function ' + _name +
    595             ' (expected: ' + maxLength + ', actual: ' + args.length + ')');
    596         err.data = {
    597           category: 'tooManyArgs',
    598           fn: _name,
    599           index: args.length,
    600           expectedLength: maxLength
    601         }
    602         return err;
    603       }
    604 
    605       err = new TypeError('Arguments of type "' + args.join(', ') +
    606           '" do not match any of the defined signatures of function ' + _name + '.');
    607       err.data = {
    608         category: 'mismatch',
    609         actual: args.map(findTypeName)
    610       }
    611       return err;
    612     }
    613 
    614     /**
    615      * Find the lowest index of all exact types of a parameter (no conversions)
    616      * @param {Param} param
    617      * @return {number} Returns the index of the lowest type in typed.types
    618      */
    619     function getLowestTypeIndex (param) {
    620       var min = 999;
    621 
    622       for (var i = 0; i < param.types.length; i++) {
    623         if (isExactType(param.types[i])) {
    624           min = Math.min(min, param.types[i].typeIndex);
    625         }
    626       }
    627 
    628       return min;
    629     }
    630 
    631     /**
    632      * Find the lowest index of the conversion of all types of the parameter
    633      * having a conversion
    634      * @param {Param} param
    635      * @return {number} Returns the lowest index of the conversions of this type
    636      */
    637     function getLowestConversionIndex (param) {
    638       var min = 999;
    639 
    640       for (var i = 0; i < param.types.length; i++) {
    641         if (!isExactType(param.types[i])) {
    642           min = Math.min(min, param.types[i].conversionIndex);
    643         }
    644       }
    645 
    646       return min;
    647     }
    648 
    649     /**
    650      * Compare two params
    651      * @param {Param} param1
    652      * @param {Param} param2
    653      * @return {number} returns a negative number when param1 must get a lower
    654      *                  index than param2, a positive number when the opposite,
    655      *                  or zero when both are equal
    656      */
    657     function compareParams (param1, param2) {
    658       var c;
    659 
    660       // compare having a rest parameter or not
    661       c = param1.restParam - param2.restParam;
    662       if (c !== 0) {
    663         return c;
    664       }
    665 
    666       // compare having conversions or not
    667       c = hasConversions(param1) - hasConversions(param2);
    668       if (c !== 0) {
    669         return c;
    670       }
    671 
    672       // compare the index of the types
    673       c = getLowestTypeIndex(param1) - getLowestTypeIndex(param2);
    674       if (c !== 0) {
    675         return c;
    676       }
    677 
    678       // compare the index of any conversion
    679       return getLowestConversionIndex(param1) - getLowestConversionIndex(param2);
    680     }
    681 
    682     /**
    683      * Compare two signatures
    684      * @param {Signature} signature1
    685      * @param {Signature} signature2
    686      * @return {number} returns a negative number when param1 must get a lower
    687      *                  index than param2, a positive number when the opposite,
    688      *                  or zero when both are equal
    689      */
    690     function compareSignatures (signature1, signature2) {
    691       var len = Math.min(signature1.params.length, signature2.params.length);
    692       var i;
    693       var c;
    694 
    695       // compare whether the params have conversions at all or not
    696       c = signature1.params.some(hasConversions) - signature2.params.some(hasConversions)
    697       if (c !== 0) {
    698         return c;
    699       }
    700 
    701       // next compare whether the params have conversions one by one
    702       for (i = 0; i < len; i++) {
    703         c = hasConversions(signature1.params[i]) - hasConversions(signature2.params[i]);
    704         if (c !== 0) {
    705           return c;
    706         }
    707       }
    708 
    709       // compare the types of the params one by one
    710       for (i = 0; i < len; i++) {
    711         c = compareParams(signature1.params[i], signature2.params[i]);
    712         if (c !== 0) {
    713           return c;
    714         }
    715       }
    716 
    717       // compare the number of params
    718       return signature1.params.length - signature2.params.length;
    719     }
    720 
    721     /**
    722      * Get params containing all types that can be converted to the defined types.
    723      *
    724      * @param {ConversionDef[]} conversions
    725      * @param {string[]} typeNames
    726      * @return {ConversionDef[]} Returns the conversions that are available
    727      *                        for every type (if any)
    728      */
    729     function filterConversions(conversions, typeNames) {
    730       var matches = {};
    731 
    732       conversions.forEach(function (conversion) {
    733         if (typeNames.indexOf(conversion.from) === -1 &&
    734             typeNames.indexOf(conversion.to) !== -1 &&
    735             !matches[conversion.from]) {
    736           matches[conversion.from] = conversion;
    737         }
    738       });
    739 
    740       return Object.keys(matches).map(function (from) {
    741         return matches[from];
    742       });
    743     }
    744 
    745     /**
    746      * Preprocess arguments before calling the original function:
    747      * - if needed convert the parameters
    748      * - in case of rest parameters, move the rest parameters into an Array
    749      * @param {Param[]} params
    750      * @param {function} fn
    751      * @return {function} Returns a wrapped function
    752      */
    753     function compileArgsPreprocessing(params, fn) {
    754       var fnConvert = fn;
    755 
    756       // TODO: can we make this wrapper function smarter/simpler?
    757 
    758       if (params.some(hasConversions)) {
    759         var restParam = hasRestParam(params);
    760         var compiledConversions = params.map(compileArgConversion)
    761 
    762         fnConvert = function convertArgs() {
    763           var args = [];
    764           var last = restParam ? arguments.length - 1 : arguments.length;
    765           for (var i = 0; i < last; i++) {
    766             args[i] = compiledConversions[i](arguments[i]);
    767           }
    768           if (restParam) {
    769             args[last] = arguments[last].map(compiledConversions[last]);
    770           }
    771 
    772           return fn.apply(this, args);
    773         }
    774       }
    775 
    776       var fnPreprocess = fnConvert;
    777       if (hasRestParam(params)) {
    778         var offset = params.length - 1;
    779 
    780         fnPreprocess = function preprocessRestParams () {
    781           return fnConvert.apply(this,
    782               slice(arguments, 0, offset).concat([slice(arguments, offset)]));
    783         }
    784       }
    785 
    786       return fnPreprocess;
    787     }
    788 
    789     /**
    790      * Compile conversion for a parameter to the right type
    791      * @param {Param} param
    792      * @return {function} Returns the wrapped function that will convert arguments
    793      *
    794      */
    795     function compileArgConversion(param) {
    796       var test0, test1, conversion0, conversion1;
    797       var tests = [];
    798       var conversions = [];
    799 
    800       param.types.forEach(function (type) {
    801         if (type.conversion) {
    802           tests.push(findTypeByName(type.conversion.from).test);
    803           conversions.push(type.conversion.convert);
    804         }
    805       });
    806 
    807       // create optimized conversion functions depending on the number of conversions
    808       switch (conversions.length) {
    809         case 0:
    810           return function convertArg(arg) {
    811             return arg;
    812           }
    813 
    814         case 1:
    815           test0 = tests[0]
    816           conversion0 = conversions[0];
    817           return function convertArg(arg) {
    818             if (test0(arg)) {
    819               return conversion0(arg)
    820             }
    821             return arg;
    822           }
    823 
    824         case 2:
    825           test0 = tests[0]
    826           test1 = tests[1]
    827           conversion0 = conversions[0];
    828           conversion1 = conversions[1];
    829           return function convertArg(arg) {
    830             if (test0(arg)) {
    831               return conversion0(arg)
    832             }
    833             if (test1(arg)) {
    834               return conversion1(arg)
    835             }
    836             return arg;
    837           }
    838 
    839         default:
    840           return function convertArg(arg) {
    841             for (var i = 0; i < conversions.length; i++) {
    842               if (tests[i](arg)) {
    843                 return conversions[i](arg);
    844               }
    845             }
    846             return arg;
    847           }
    848       }
    849     }
    850 
    851     /**
    852      * Convert an array with signatures into a map with signatures,
    853      * where signatures with union types are split into separate signatures
    854      *
    855      * Throws an error when there are conflicting types
    856      *
    857      * @param {Signature[]} signatures
    858      * @return {Object.<string, function>}  Returns a map with signatures
    859      *                                      as key and the original function
    860      *                                      of this signature as value.
    861      */
    862     function createSignaturesMap(signatures) {
    863       var signaturesMap = {};
    864       signatures.forEach(function (signature) {
    865         if (!signature.params.some(hasConversions)) {
    866           splitParams(signature.params, true).forEach(function (params) {
    867             signaturesMap[stringifyParams(params)] = signature.fn;
    868           });
    869         }
    870       });
    871 
    872       return signaturesMap;
    873     }
    874 
    875     /**
    876      * Split params with union types in to separate params.
    877      *
    878      * For example:
    879      *
    880      *     splitParams([['Array', 'Object'], ['string', 'RegExp'])
    881      *     // returns:
    882      *     // [
    883      *     //   ['Array', 'string'],
    884      *     //   ['Array', 'RegExp'],
    885      *     //   ['Object', 'string'],
    886      *     //   ['Object', 'RegExp']
    887      *     // ]
    888      *
    889      * @param {Param[]} params
    890      * @param {boolean} ignoreConversionTypes
    891      * @return {Param[]}
    892      */
    893     function splitParams(params, ignoreConversionTypes) {
    894       function _splitParams(params, index, types) {
    895         if (index < params.length) {
    896           var param = params[index]
    897           var filteredTypes = ignoreConversionTypes
    898               ? param.types.filter(isExactType)
    899               : param.types;
    900           var typeGroups
    901 
    902           if (param.restParam) {
    903             // split the types of a rest parameter in two:
    904             // one with only exact types, and one with exact types and conversions
    905             var exactTypes = filteredTypes.filter(isExactType)
    906             typeGroups = exactTypes.length < filteredTypes.length
    907                 ? [exactTypes, filteredTypes]
    908                 : [filteredTypes]
    909 
    910           }
    911           else {
    912             // split all the types of a regular parameter into one type per group
    913             typeGroups = filteredTypes.map(function (type) {
    914               return [type]
    915             })
    916           }
    917 
    918           // recurse over the groups with types
    919           return flatMap(typeGroups, function (typeGroup) {
    920             return _splitParams(params, index + 1, types.concat([typeGroup]));
    921           });
    922 
    923         }
    924         else {
    925           // we've reached the end of the parameters. Now build a new Param
    926           var splittedParams = types.map(function (type, typeIndex) {
    927             return {
    928               types: type,
    929               restParam: (typeIndex === params.length - 1) && hasRestParam(params)
    930             }
    931           });
    932 
    933           return [splittedParams];
    934         }
    935       }
    936 
    937       return _splitParams(params, 0, []);
    938     }
    939 
    940     /**
    941      * Test whether two signatures have a conflicting signature
    942      * @param {Signature} signature1
    943      * @param {Signature} signature2
    944      * @return {boolean} Returns true when the signatures conflict, false otherwise.
    945      */
    946     function hasConflictingParams(signature1, signature2) {
    947       var ii = Math.max(signature1.params.length, signature2.params.length);
    948 
    949       for (var i = 0; i < ii; i++) {
    950         var typesNames1 = getExpectedTypeNames(signature1, i, true);
    951         var typesNames2 = getExpectedTypeNames(signature2, i, true);
    952 
    953         if (!hasOverlap(typesNames1, typesNames2)) {
    954           return false;
    955         }
    956       }
    957 
    958       var len1 = signature1.params.length;
    959       var len2 = signature2.params.length;
    960       var restParam1 = hasRestParam(signature1.params);
    961       var restParam2 = hasRestParam(signature2.params);
    962 
    963       return restParam1
    964           ? restParam2 ? (len1 === len2) : (len2 >= len1)
    965           : restParam2 ? (len1 >= len2)  : (len1 === len2)
    966     }
    967 
    968     /**
    969      * Create a typed function
    970      * @param {String} name               The name for the typed function
    971      * @param {Object.<string, function>} signaturesMap
    972      *                                    An object with one or
    973      *                                    multiple signatures as key, and the
    974      *                                    function corresponding to the
    975      *                                    signature as value.
    976      * @return {function}  Returns the created typed function.
    977      */
    978     function createTypedFunction(name, signaturesMap) {
    979       if (Object.keys(signaturesMap).length === 0) {
    980         throw new SyntaxError('No signatures provided');
    981       }
    982 
    983       // parse the signatures, and check for conflicts
    984       var parsedSignatures = [];
    985       Object.keys(signaturesMap)
    986           .map(function (signature) {
    987             return parseSignature(signature, signaturesMap[signature], typed.conversions);
    988           })
    989           .filter(notNull)
    990           .forEach(function (parsedSignature) {
    991             // check whether this parameter conflicts with already parsed signatures
    992             var conflictingSignature = findInArray(parsedSignatures, function (s) {
    993               return hasConflictingParams(s, parsedSignature)
    994             });
    995             if (conflictingSignature) {
    996               throw new TypeError('Conflicting signatures "' +
    997                   stringifyParams(conflictingSignature.params) + '" and "' +
    998                   stringifyParams(parsedSignature.params) + '".');
    999             }
   1000 
   1001             parsedSignatures.push(parsedSignature);
   1002           });
   1003 
   1004       // split and filter the types of the signatures, and then order them
   1005       var signatures = flatMap(parsedSignatures, function (parsedSignature) {
   1006         var params = parsedSignature ? splitParams(parsedSignature.params, false) : []
   1007 
   1008         return params.map(function (params) {
   1009           return {
   1010             params: params,
   1011             fn: parsedSignature.fn
   1012           };
   1013         });
   1014       }).filter(notNull);
   1015 
   1016       signatures.sort(compareSignatures);
   1017 
   1018       // we create a highly optimized checks for the first couple of signatures with max 2 arguments
   1019       var ok0 = signatures[0] && signatures[0].params.length <= 2 && !hasRestParam(signatures[0].params);
   1020       var ok1 = signatures[1] && signatures[1].params.length <= 2 && !hasRestParam(signatures[1].params);
   1021       var ok2 = signatures[2] && signatures[2].params.length <= 2 && !hasRestParam(signatures[2].params);
   1022       var ok3 = signatures[3] && signatures[3].params.length <= 2 && !hasRestParam(signatures[3].params);
   1023       var ok4 = signatures[4] && signatures[4].params.length <= 2 && !hasRestParam(signatures[4].params);
   1024       var ok5 = signatures[5] && signatures[5].params.length <= 2 && !hasRestParam(signatures[5].params);
   1025       var allOk = ok0 && ok1 && ok2 && ok3 && ok4 && ok5;
   1026 
   1027       // compile the tests
   1028       var tests = signatures.map(function (signature) {
   1029         return compileTests(signature.params);
   1030       });
   1031 
   1032       var test00 = ok0 ? compileTest(signatures[0].params[0]) : notOk;
   1033       var test10 = ok1 ? compileTest(signatures[1].params[0]) : notOk;
   1034       var test20 = ok2 ? compileTest(signatures[2].params[0]) : notOk;
   1035       var test30 = ok3 ? compileTest(signatures[3].params[0]) : notOk;
   1036       var test40 = ok4 ? compileTest(signatures[4].params[0]) : notOk;
   1037       var test50 = ok5 ? compileTest(signatures[5].params[0]) : notOk;
   1038 
   1039       var test01 = ok0 ? compileTest(signatures[0].params[1]) : notOk;
   1040       var test11 = ok1 ? compileTest(signatures[1].params[1]) : notOk;
   1041       var test21 = ok2 ? compileTest(signatures[2].params[1]) : notOk;
   1042       var test31 = ok3 ? compileTest(signatures[3].params[1]) : notOk;
   1043       var test41 = ok4 ? compileTest(signatures[4].params[1]) : notOk;
   1044       var test51 = ok5 ? compileTest(signatures[5].params[1]) : notOk;
   1045 
   1046       // compile the functions
   1047       var fns = signatures.map(function(signature) {
   1048         return compileArgsPreprocessing(signature.params, signature.fn);
   1049       });
   1050 
   1051       var fn0 = ok0 ? fns[0] : undef;
   1052       var fn1 = ok1 ? fns[1] : undef;
   1053       var fn2 = ok2 ? fns[2] : undef;
   1054       var fn3 = ok3 ? fns[3] : undef;
   1055       var fn4 = ok4 ? fns[4] : undef;
   1056       var fn5 = ok5 ? fns[5] : undef;
   1057 
   1058       var len0 = ok0 ? signatures[0].params.length : -1;
   1059       var len1 = ok1 ? signatures[1].params.length : -1;
   1060       var len2 = ok2 ? signatures[2].params.length : -1;
   1061       var len3 = ok3 ? signatures[3].params.length : -1;
   1062       var len4 = ok4 ? signatures[4].params.length : -1;
   1063       var len5 = ok5 ? signatures[5].params.length : -1;
   1064 
   1065       // simple and generic, but also slow
   1066       var iStart = allOk ? 6 : 0;
   1067       var iEnd = signatures.length;
   1068       var generic = function generic() {
   1069         'use strict';
   1070 
   1071         for (var i = iStart; i < iEnd; i++) {
   1072           if (tests[i](arguments)) {
   1073             return fns[i].apply(this, arguments);
   1074           }
   1075         }
   1076 
   1077         return typed.onMismatch(name, arguments, signatures);
   1078       }
   1079 
   1080       // create the typed function
   1081       // fast, specialized version. Falls back to the slower, generic one if needed
   1082       var fn = function fn(arg0, arg1) {
   1083         'use strict';
   1084 
   1085         if (arguments.length === len0 && test00(arg0) && test01(arg1)) { return fn0.apply(fn, arguments); }
   1086         if (arguments.length === len1 && test10(arg0) && test11(arg1)) { return fn1.apply(fn, arguments); }
   1087         if (arguments.length === len2 && test20(arg0) && test21(arg1)) { return fn2.apply(fn, arguments); }
   1088         if (arguments.length === len3 && test30(arg0) && test31(arg1)) { return fn3.apply(fn, arguments); }
   1089         if (arguments.length === len4 && test40(arg0) && test41(arg1)) { return fn4.apply(fn, arguments); }
   1090         if (arguments.length === len5 && test50(arg0) && test51(arg1)) { return fn5.apply(fn, arguments); }
   1091 
   1092         return generic.apply(fn, arguments);
   1093       }
   1094 
   1095       // attach name the typed function
   1096       try {
   1097         Object.defineProperty(fn, 'name', {value: name});
   1098       }
   1099       catch (err) {
   1100         // old browsers do not support Object.defineProperty and some don't support setting the name property
   1101         // the function name is not essential for the functioning, it's mostly useful for debugging,
   1102         // so it's fine to have unnamed functions.
   1103       }
   1104 
   1105       // attach signatures to the function
   1106       fn.signatures = createSignaturesMap(signatures);
   1107 
   1108       return fn;
   1109     }
   1110 
   1111     /**
   1112      * Action to take on mismatch
   1113      * @param {string} name      Name of function that was attempted to be called
   1114      * @param {Array} args       Actual arguments to the call
   1115      * @param {Array} signatures Known signatures of the named typed-function
   1116      */
   1117     function _onMismatch(name, args, signatures) {
   1118       throw createError(name, args, signatures);
   1119     }
   1120 
   1121     /**
   1122      * Test whether a type should be NOT be ignored
   1123      * @param {string} typeName
   1124      * @return {boolean}
   1125      */
   1126     function notIgnore(typeName) {
   1127       return typed.ignore.indexOf(typeName) === -1;
   1128     }
   1129 
   1130     /**
   1131      * trim a string
   1132      * @param {string} str
   1133      * @return {string}
   1134      */
   1135     function trim(str) {
   1136       return str.trim();
   1137     }
   1138 
   1139     /**
   1140      * Test whether a string is not empty
   1141      * @param {string} str
   1142      * @return {boolean}
   1143      */
   1144     function notEmpty(str) {
   1145       return !!str;
   1146     }
   1147 
   1148     /**
   1149      * test whether a value is not strict equal to null
   1150      * @param {*} value
   1151      * @return {boolean}
   1152      */
   1153     function notNull(value) {
   1154       return value !== null;
   1155     }
   1156 
   1157     /**
   1158      * Test whether a parameter has no types defined
   1159      * @param {Param} param
   1160      * @return {boolean}
   1161      */
   1162     function isInvalidParam (param) {
   1163       return param.types.length === 0;
   1164     }
   1165 
   1166     /**
   1167      * Return all but the last items of an array
   1168      * @param {Array} arr
   1169      * @return {Array}
   1170      */
   1171     function initial(arr) {
   1172       return arr.slice(0, arr.length - 1);
   1173     }
   1174 
   1175     /**
   1176      * return the last item of an array
   1177      * @param {Array} arr
   1178      * @return {*}
   1179      */
   1180     function last(arr) {
   1181       return arr[arr.length - 1];
   1182     }
   1183 
   1184     /**
   1185      * Slice an array or function Arguments
   1186      * @param {Array | Arguments | IArguments} arr
   1187      * @param {number} start
   1188      * @param {number} [end]
   1189      * @return {Array}
   1190      */
   1191     function slice(arr, start, end) {
   1192       return Array.prototype.slice.call(arr, start, end);
   1193     }
   1194 
   1195     /**
   1196      * Test whether an array contains some item
   1197      * @param {Array} array
   1198      * @param {*} item
   1199      * @return {boolean} Returns true if array contains item, false if not.
   1200      */
   1201     function contains(array, item) {
   1202       return array.indexOf(item) !== -1;
   1203     }
   1204 
   1205     /**
   1206      * Test whether two arrays have overlapping items
   1207      * @param {Array} array1
   1208      * @param {Array} array2
   1209      * @return {boolean} Returns true when at least one item exists in both arrays
   1210      */
   1211     function hasOverlap(array1, array2) {
   1212       for (var i = 0; i < array1.length; i++) {
   1213         if (contains(array2, array1[i])) {
   1214           return true;
   1215         }
   1216       }
   1217 
   1218       return false;
   1219     }
   1220 
   1221     /**
   1222      * Return the first item from an array for which test(arr[i]) returns true
   1223      * @param {Array} arr
   1224      * @param {function} test
   1225      * @return {* | undefined} Returns the first matching item
   1226      *                         or undefined when there is no match
   1227      */
   1228     function findInArray(arr, test) {
   1229       for (var i = 0; i < arr.length; i++) {
   1230         if (test(arr[i])) {
   1231           return arr[i];
   1232         }
   1233       }
   1234       return undefined;
   1235     }
   1236 
   1237     /**
   1238      * Filter unique items of an array with strings
   1239      * @param {string[]} arr
   1240      * @return {string[]}
   1241      */
   1242     function uniq(arr) {
   1243       var entries = {}
   1244       for (var i = 0; i < arr.length; i++) {
   1245         entries[arr[i]] = true;
   1246       }
   1247       return Object.keys(entries);
   1248     }
   1249 
   1250     /**
   1251      * Flat map the result invoking a callback for every item in an array.
   1252      * https://gist.github.com/samgiles/762ee337dff48623e729
   1253      * @param {Array} arr
   1254      * @param {function} callback
   1255      * @return {Array}
   1256      */
   1257     function flatMap(arr, callback) {
   1258       return Array.prototype.concat.apply([], arr.map(callback));
   1259     }
   1260 
   1261     /**
   1262      * Retrieve the function name from a set of typed functions,
   1263      * and check whether the name of all functions match (if given)
   1264      * @param {function[]} fns
   1265      */
   1266     function getName (fns) {
   1267       var name = '';
   1268 
   1269       for (var i = 0; i < fns.length; i++) {
   1270         var fn = fns[i];
   1271 
   1272         // check whether the names are the same when defined
   1273         if ((typeof fn.signatures === 'object' || typeof fn.signature === 'string') && fn.name !== '') {
   1274           if (name === '') {
   1275             name = fn.name;
   1276           }
   1277           else if (name !== fn.name) {
   1278             var err = new Error('Function names do not match (expected: ' + name + ', actual: ' + fn.name + ')');
   1279             err.data = {
   1280               actual: fn.name,
   1281               expected: name
   1282             };
   1283             throw err;
   1284           }
   1285         }
   1286       }
   1287 
   1288       return name;
   1289     }
   1290 
   1291     // extract and merge all signatures of a list with typed functions
   1292     function extractSignatures(fns) {
   1293       var err;
   1294       var signaturesMap = {};
   1295 
   1296       function validateUnique(_signature, _fn) {
   1297         if (signaturesMap.hasOwnProperty(_signature) && _fn !== signaturesMap[_signature]) {
   1298           err = new Error('Signature "' + _signature + '" is defined twice');
   1299           err.data = {signature: _signature};
   1300           throw err;
   1301           // else: both signatures point to the same function, that's fine
   1302         }
   1303       }
   1304 
   1305       for (var i = 0; i < fns.length; i++) {
   1306         var fn = fns[i];
   1307 
   1308         // test whether this is a typed-function
   1309         if (typeof fn.signatures === 'object') {
   1310           // merge the signatures
   1311           for (var signature in fn.signatures) {
   1312             if (fn.signatures.hasOwnProperty(signature)) {
   1313               validateUnique(signature, fn.signatures[signature]);
   1314               signaturesMap[signature] = fn.signatures[signature];
   1315             }
   1316           }
   1317         }
   1318         else if (typeof fn.signature === 'string') {
   1319           validateUnique(fn.signature, fn);
   1320           signaturesMap[fn.signature] = fn;
   1321         }
   1322         else {
   1323           err = new TypeError('Function is no typed-function (index: ' + i + ')');
   1324           err.data = {index: i};
   1325           throw err;
   1326         }
   1327       }
   1328 
   1329       return signaturesMap;
   1330     }
   1331 
   1332     typed = createTypedFunction('typed', {
   1333       'string, Object': createTypedFunction,
   1334       'Object': function (signaturesMap) {
   1335         // find existing name
   1336         var fns = [];
   1337         for (var signature in signaturesMap) {
   1338           if (signaturesMap.hasOwnProperty(signature)) {
   1339             fns.push(signaturesMap[signature]);
   1340           }
   1341         }
   1342         var name = getName(fns);
   1343         return createTypedFunction(name, signaturesMap);
   1344       },
   1345       '...Function': function (fns) {
   1346         return createTypedFunction(getName(fns), extractSignatures(fns));
   1347       },
   1348       'string, ...Function': function (name, fns) {
   1349         return createTypedFunction(name, extractSignatures(fns));
   1350       }
   1351     });
   1352 
   1353     typed.create = create;
   1354     typed.types = _types;
   1355     typed.conversions = _conversions;
   1356     typed.ignore = _ignore;
   1357     typed.onMismatch = _onMismatch;
   1358     typed.throwMismatchError = _onMismatch;
   1359     typed.createError = createError;
   1360     typed.convert = convert;
   1361     typed.find = find;
   1362 
   1363     /**
   1364      * add a type
   1365      * @param {{name: string, test: function}} type
   1366      * @param {boolean} [beforeObjectTest=true]
   1367      *                          If true, the new test will be inserted before
   1368      *                          the test with name 'Object' (if any), since
   1369      *                          tests for Object match Array and classes too.
   1370      */
   1371     typed.addType = function (type, beforeObjectTest) {
   1372       if (!type || typeof type.name !== 'string' || typeof type.test !== 'function') {
   1373         throw new TypeError('Object with properties {name: string, test: function} expected');
   1374       }
   1375 
   1376       if (beforeObjectTest !== false) {
   1377         for (var i = 0; i < typed.types.length; i++) {
   1378           if (typed.types[i].name === 'Object') {
   1379             typed.types.splice(i, 0, type);
   1380             return
   1381           }
   1382         }
   1383       }
   1384 
   1385       typed.types.push(type);
   1386     };
   1387 
   1388     // add a conversion
   1389     typed.addConversion = function (conversion) {
   1390       if (!conversion
   1391           || typeof conversion.from !== 'string'
   1392           || typeof conversion.to !== 'string'
   1393           || typeof conversion.convert !== 'function') {
   1394         throw new TypeError('Object with properties {from: string, to: string, convert: function} expected');
   1395       }
   1396 
   1397       typed.conversions.push(conversion);
   1398     };
   1399 
   1400     return typed;
   1401   }
   1402 
   1403   return create();
   1404 }));