const { parse } = require('@babel/parser');
const makeInvariantFromNode = node => {
switch (node.type) {
case 'BooleanLiteralTypeAnnotation':
case 'StringLiteralTypeAnnotation':
case 'NumberLiteralTypeAnnotation': {
const { value } = node;
return x => x === value;
}
case 'StringTypeAnnotation':
return x => typeof x === 'string';
case 'NumberTypeAnnotation':
return x => typeof x === 'number';
case 'BooleanTypeAnnotation':
return x => typeof x === 'boolean';
case 'NullLiteralTypeAnnotation':
return x => x === null;
case 'VoidTypeAnnotation':
return x => x === undefined;
case 'NullableTypeAnnotation': {
const inner = makeInvariantFromNode(node.typeAnnotation);
return val => val === null || val === undefined || inner(val);
}
case 'ArrayTypeAnnotation': {
const inner = makeInvariantFromNode(node.elementType);
return val => Array.isArray(val) && val.every(inner);
}
case 'GenericTypeAnnotation': {
const { name } = node.id;
if (name === 'Function')
return val => typeof val === 'function'
else if (name === 'Object')
return val => typeof val === 'object'
else
return val =>
typeof val === 'object' && val.constructor.name === name;
}
case 'IntersectionTypeAnnotation': {
const inner = node.types.map(makeInvariantFromNode);
return val => inner.every(i => i(val));
}
case 'UnionTypeAnnotation': {
const inner = node.types.map(makeInvariantFromNode);
return val => inner.some(i => i(val));
}
case 'TupleTypeAnnotation': {
const values = node.types.map(makeInvariantFromNode);
return val =>
Array.isArray(val)
&& val.length === values.length
&& val.every((x, i) => values[i](x));
}
case 'ObjectTypeAnnotation': {
const properties = node.properties
.filter(n => n.type === 'ObjectTypeProperty')
.map(n => [n.key.name, makeInvariantFromNode(n.value)]);
return val => typeof val === 'object' && properties.every(([key, inner]) => inner(val[key]));
}
case 'AnyTypeAnnotation':
return () => true;
default:
throw new TypeError(`Unsupported type annotation ${node.type}`);
}
};
const makeInvariantForFn = (paramTypes, returnType) => fn => {
const paramSize = paramTypes.length;
const paramInvariants = paramTypes.map(makeInvariantFromNode);
const returnInvariant = makeInvariantFromNode(returnType);
if (paramSize !== fn.length) {
throw new TypeError('Arity of type annotation and actual arguments do not match');
}
return (...args) => {
if (paramSize !== args.length) {
throw new TypeError('Arity of type annotation and passed arguments do not match');
}
for (let i = 0; i < paramSize; i++) {
if (!paramInvariants[i](args[i])) {
const typeReadable = paramTypes[i].type.replace('Annotation', '');
throw new TypeError(`Invalid ${i + 1}. argument, expected ${typeReadable}.`);
}
}
const res = fn(...args);
if (!returnInvariant(res)) {
const typeReadable = returnType.type.replace('Annotation', '');
throw new TypeError(`Invalid return value, expected ${typeReadable}.`);
}
return res;
};
};
const sig = def => {
const input = `type x = ${def}`;
const { program: { body } } = parse(input, { plugins: ['flow'] });
return body[0];
/*
const node = body[0];
if (!node || !node.right || node.right.type !== 'FunctionTypeAnnotation') {
throw new TypeError(`Expected valid Flow function annotation.`);
}
const { params, returnType } = node.right;
const paramTypes = params.map(n => n.typeAnnotation);
console.log(paramTypes);
return makeInvariantForFn(paramTypes, returnType);
*/
};
sig`
{ message: ?string }
`
// fun('test')