I like typed code, and I don't like the idea of introducing a new syntax and a new toolchain to support it in Javascript. This eslint plugin is an attempt to reconcile those two facts. It works by examining JSDoc comments attached to your variables and functions, to (attempt to) make sure you're not accidentally using the wrong datatype.
This plugin is not, and will never be, a complete replacement for TypeScript. But if all you want is to avoid some basic type-related footguns then it's probably good enough.
/**
* @param {boolean} myBool
* @return {string}
*/
function booleanIsTrue(myBool) {
return myBool ? "yes" : "no";
}
/** @type {number} */
const x = booleanIsTrue(true);
// eslint message:
// "can't assign type string to variable of type number"
/** @type {string} */
const y = booleanIsTrue(true);
// correctly-typed, so no eslint message! ๐
const z = booleanIsTrue(false);
// also no eslint message, since `z` has no declared type ๐ค (you should fix that!)
this plugin requires eslint, so be sure to install that too if you're not already using it:
$ npm install --save-dev eslint @orzechowskid/eslint-plugin-typelint
it's highly recommended that you use eslint-plugin-jsdoc as well, to ensure your type definitions are well-formed:
$ npm install --save-dev eslint-plugin-jsdoc
add one or more typelint
rules to your eslint config file:
module.exports = {
"plugins": [
"jsdoc",
"@orzechowskid/typelint"
],
"parserOptions": {
"ecmaFeatures": {
"impliedStrict": true
},
"ecmaVersion": 8,
"loc": true
},
"rules": {
"@orzechowskid/typelint/assignment-types-must-match": [ "error" ]
}
};
ensures that the types on the left-hand and right-hand sides of a statement match when initializing or assigning to a variable
none
// does not pass - attempting to assign a number to a variable declared as a boolean
/** @type {boolean} */
const myBoolean = 123;
// does not pass - variable declared as a boolean but function's return value is a string
/** @type {boolean} */
const myBoolean = someFunctionReturningAString();
// does not pass - variable declared as boolean but actual runtime value is either boolean or undefined
/** @type {boolean} */
const myBoolean = someTest ? true : undefined;
// passes
/** @type {boolean} */
const myBoolean = someTest ? true : false;
// passes
/** @type {boolean} */
const myBoolean = someFunctionReturningABoolean();
// passes
/** @type {boolean|undefined} */
const myBoolean = someTest ? true : undefined;
ensures that a function is always called with the number of parameters it expects.
none
/**
* @param {number} a
* @param {number} b
* @return {number}
*/
function myFunction(a, b) {
return a ^ b;
}
// does not pass - function expects 2 arguments but was only given 1
const myNum = myFunction(10);
// does not pass - function expects 2 arguments but was given 3
const myNum = myFunction(10, 7, 12);
// passes
const myNum = myFunction(10, 7);
ensures that a function's arguments match the types documented in its JSDoc block
when set to true
, this rule will not type-check any implicit parameters to the function call (where an 'implicit parameter' is what you get when e.g. calling myFun(x, y)
with only one arg. the value y
will be set to undefined
). Default: false
/**
* @param {string} name
* @param {number} value
* @return {string}
*/
function appendValue(name, value) {
return `${name}: ${value}`;
}
// does not pass - the first parameter should be a string
const myStr = appendValue(123, 'Alice');
// does not pass by default - the implicit second parameter is of type undefined
const myStr = appendValue('Alice');
// passes
const myStr = appendValue('Alice', 123);
// passes if the `ignoreTrailingUndefineds` option is set to true
const myStr = appendValue('Alice');
ensures that a function returns the value it says it will return in its documentation
none
// does not pass - function says it returns a string but it actually returns a boolean
/**
* @param {any} obj
* @return {string}
*/
function toString(obj) {
return !!obj.toString();
}
// does not pass - object literal does not match typedef
/**
* @typedef {object} MyRecord
* @property {string} name
* @property {number} age
*/
/**
* @return {MyRecord}
*/
function getRecord() {
return {
name: 'Eric',
age: 'none of your business'
};
}
// does not pass - return value could either be a string or undefined
/**
* @param {any} obj
* @return {string}
*/
function toString(obj) {
return obj ? obj.toString() : undefined;
}
// passes
/**
* @param {any} obj
* @return {string}
*/
function toString(obj) {
return obj ? obj.toString() : 'does not exist';
}
// passes
/**
* @return {Record}
*/
function getRecord() {
return {
name: 'Eric',
age: 71
};
}
// passes
/**
* @param {any} obj
* @return {string|undefined}
*/
function toString(obj) {
return obj ? obj.toString() : undefined;
}
probably lots! I'm not necessarily proud of this code!
you can file an issue here if something doesn't work the way you think it should, or even better: open a pull request here.
MIT