Skip to content

Instantly share code, notes, and snippets.

@ne-sachirou
Last active August 29, 2015 14:11
Show Gist options
  • Save ne-sachirou/562a7bd59772147a83df to your computer and use it in GitHub Desktop.
Save ne-sachirou/562a7bd59772147a83df to your computer and use it in GitHub Desktop.
JS コメントで型注釈を書き実行時に自動で型検査
TypeScriptやFlowやAtScriptやES6を使はずに、関数にコメントを書くことで型註釋[注釈]を行なひ、
実行時に自動で検査します。
例:
var f = typed(function (a/*:string*/)/*:number*/ {
return a;
});
var g = typed(function (a/*:number*/, b/*:number?*/)/*:number*/ {
b = b || 0;
return a + b;
});
var h = typed(function (a/*:A*/)/*:A*/ {
return a;
});
License: CC 0 or Public Domain
* {
margin: 0;
padding: 0;
border: 0;
}
body {
background: #dff;
font: 30px sans-serif;
}
.error {
color: red;
}
<div id='console'></div>
/**
* @author ne_Sachirou <[email protected]>
* @date 2014 - 2014
* @license CC 0 or Public Domain
*/
var assert;
if (this.require) {
assert = require('assert');
} else {
assert = function () {};
assert.throws = function () {};
}
// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
if (!String.prototype.endsWith) {
Object.defineProperty(String.prototype, 'endsWith', {
enumerable: false,
configurable: false,
writable: false,
value: function (searchString, position) {
position = position || this.length;
position = position - searchString.length;
if (position < 0) { position = 0; } // MDN's polyfill has a bug.
return this.lastIndexOf(searchString) === position;
}
});
}
if (this.document) {
console.log = function (obj) {
var node = document.getElementById('console');
node.innerHTML += '<div>'+obj+'</div>';
};
console.error = function (err) {
var node = document.getElementById('console');
node.innerHTML += '<div class="error">'+err+'</div>';
};
}
(function (global) {
'use strict';
// https://plus.google.com/u/0/+InoueSachirou/posts/cGtJLikXC4P
// https://gist.github.com/SirAnthony/e1c7b9522e7ab02c6679
// http://jsperf.com/typeof-v-s-instanceof
var typeCheckers = {
string : function (obj) { return 'string' === typeof obj || obj instanceof String; },
'String' : function (obj) { return 'string' === typeof obj || obj instanceof String; },
number : function (obj) { return 'number' === typeof obj || obj instanceof Number; },
'Number' : function (obj) { return 'number' === typeof obj || obj instanceof Number; },
'boolean' : function (obj) { return 'boolean' === typeof obj || obj instanceof Boolean; },
'Boolean' : function (obj) { return 'boolean' === typeof obj || obj instanceof Boolean; },
'function' : function (obj) { return 'function' === typeof obj; },
object : function (obj) { return 'object' === typeof obj; },
array : Array.isArray,
};
function checkType(obj/*:object?*/, type/*:string?*/)/*:undefined*/ {
var constructor, i = 0, iz = 0;
if (!type) { return; }
if (type[type.length - 1] === '?') {
if (null === obj || void 0 === obj) { return; }
type = type.slice(0, type.length - 1);
} else if (null === obj || void 0 === obj) {
throw new TypeError(obj+' is not a '+type);
}
if (type.endsWith('[]')) {
if (!Array.isArray(obj)) {
throw new TypeError(obj+' is not a '+type);
}
type = type.slice(0, type.length - 2);
for (i = 0, iz = obj.length; i < iz; ++i) {
checkType(obj[i], type);
}
return;
}
if (typeCheckers[type]) {
if (typeCheckers[type](obj)) { return; }
throw new TypeError(obj+' is not a '+type);
}
while (obj) {
if (obj.constructor.name === type) { return; }
obj = obj.__proto__;
}
throw new TypeError(obj+' is not a '+type);
}
// {{{!test
(function () {
checkType('A', 'string');
checkType('', 'string');
checkType(String(''), 'string');
checkType(new String(''), 'string');
checkType('A', 'String');
checkType('', 'String');
checkType(String(''), 'String');
checkType(new String(''), 'String');
checkType(1, 'number');
checkType(0, 'number');
checkType(Number(0), 'number');
checkType(new Number(0), 'number');
checkType(NaN, 'number');
checkType(Infinity, 'number');
checkType(1, 'Number');
checkType(0, 'Number');
checkType(Number(0), 'Number');
checkType(new Number(0), 'Number');
checkType(NaN, 'Number');
checkType(Infinity, 'Number');
checkType(true, 'boolean');
checkType(false, 'boolean');
checkType(Boolean(false), 'boolean');
checkType(new Boolean(false), 'boolean');
checkType(true, 'Boolean');
checkType(false, 'Boolean');
checkType(Boolean(false), 'Boolean');
checkType(new Boolean(false), 'Boolean');
checkType(function () {}, 'function');
checkType(Function(''), 'function');
checkType(new Function(''), 'function');
checkType(function () {}, 'Function');
checkType(Function(''), 'Function');
checkType(new Function(''), 'Function');
checkType({}, 'object');
checkType(Object(), 'object');
checkType(new Object(), 'object');
checkType({}, 'Object');
checkType(Object(), 'Object');
checkType(new Object(), 'Object');
checkType([], 'array');
checkType(Array(), 'array');
checkType(new Array(), 'array');
checkType([], 'Array');
checkType(Array(), 'Array');
checkType(new Array(), 'Array');
checkType(/r/, 'RegExp');
checkType(RegExp(''), 'RegExp');
checkType(new RegExp(''), 'RegExp');
function Parent() { }
function Child() { Parent.call(this); }
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
checkType(new Parent, 'Parent');
checkType(new Child, 'Child');
checkType(new Child, 'Parent');
assert.throws(function () { checkType(0, 'string'); }, TypeError);
assert.throws(function () { checkType(new Parent, 'Child'); }, TypeError);
assert.throws(function () { checkType(null, 'number'); }, TypeError);
checkType(null, 'number?');
checkType(void 0, 'number?');
checkType(0, 'number?');
checkType([0], 'number[]');
checkType([], 'number[]');
assert.throws(function () { checkType(0, 'number[]'); }, TypeError);
assert.throws(function () { checkType([0, ''], 'number[]'); }, TypeError);
}());
// }}}!test
function typed(func/*:function*/)/*:function*/ {
var params, returnType;
params = func.toString().
replace(/\r?\n/g, ' ').
match(/function[^\(]*\(([^\)]*)\)/)[1].
split(',').
map(function (param) {
var m;
m = param.trim().match(/\/\*:([\w?]+)\*\/$/);
return m ? m[1] : void 0;
});
returnType = func.toString().match(/function[^\(]*\([^\)]*\)([^{]*){/)[1].
trim().
match(/\/\*:([\w?]+)\*\//);
returnType = returnType ? returnType[1] : void 0;
return function () {
var result, i = 0, iz = 0;
for (i = 0, iz = arguments.length; i < iz; ++i) {
checkType(arguments[i], params[i]);
}
result = func.apply(null, arguments);
checkType(result, returnType);
return result;
};
}
global.typed = typed;
}(this.self || global));
// ==================== ここから使用例 ====================
var f = typed(function (a/*:string*/)/*:number*/ {
return a;
});
try { f('str'); } catch (err) { console.error(err); }
try { f(42); } catch (err) { console.error(err); }
var g = typed(function (a/*:number*/, b/*:number?*/)/*:number*/ {
b = b || 0;
return a + b;
});
console.log(g(5, 6));
console.log(g(5));
try { g(5, '') } catch (err) { console.error(err); }
function A() {
this.a = 42;
}
A.prototype.toString = function () {
return 'A{a='+this.a+'}';
};
var h = typed(function (a/*:A*/)/*:A*/ {
return a;
});
console.log(h(new A()));
try { h(42); } catch (err) { console.error(err); }
try { h(null); } catch (err) { console.error(err); }
try { h(); } catch (err) { console.error(err); }
// vim:fdm=marker:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment