@samldev/eslint-plugin-proper-ternary
v3.2.2
Published
ESLint rules to ensure proper usage of ternary/conditional expressions
Downloads
9
Maintainers
Readme
ESLint Plugin: proper-ternary
Overview
The proper-ternary ESLint plugin provides rules that control the definitions of ? :
conditional expressions (aka, "ternary expressions"), restricting them to a narrower and more proper/readable form.
The rules defined in this plugin:
"nested"
: controls the nesting of? :
ternary expressions."parens"
: requires surrounding( .. )
parentheses around specific kinds of expressions in ternary expression clauses."where"
: restricts where in program structure ternary expressions can be used: forbidding them as standalone statements, in object properties, as arguments, etc.
Enabling The Plugin
To use proper-ternary, load it as a plugin into ESLint and configure the rules as desired.
extends
If you'd like to use the proper-ternary plugin in a recommended configuration preset, you can add the plugin in the extends
clause of your ESLint configuration, and pick a preset by name:
"extends": [
// ..
"plugin:@samldev/proper-ternary/CONFIG-PRESET-NAME",
// ..
]
Note: All included configuration presets not only define specific rule configurations but also automatically load the plugin itself, so you don't need to list proper-ternary in the plugins
clause.
The available configuration presets to choose from:
getify-says
: This is my personal configuration. See the preset definition...TBA..
It's important to note that you can still override any of the preset rule definitions in your configuration. Think of these presets as convenience "defaults" that can still be customized.
.eslintrc.json
To load the plugin and enable its rules via a local or global .eslintrc.json
configuration file:
"plugins": [
"@samldev/proper-ternary"
],
"rules": {
"@samldev/proper-ternary/nested": "error",
"@samldev/proper-ternary/parens": "error",
"@samldev/proper-ternary/where": "error"
}
package.json
To load the plugin and enable its rules via a project's package.json
:
"eslintConfig": {
"plugins": [
"@samldev/proper-ternary"
],
"rules": {
"@samldev/proper-ternary/nested": "error",
"@samldev/proper-ternary/parens": "error",
"@samldev/proper-ternary/where": "error"
}
}
ESLint CLI parameters
To load the plugin and enable its rules via ESLint CLI parameters, use --plugin
and --rule
flags:
eslint .. --plugin='@samldev/proper-ternary' --rule='@samldev/proper-ternary/nested: error' ..
eslint .. --plugin='@samldev/proper-ternary' --rule='@samldev/proper-ternary/parens: error' ..
eslint .. --plugin='@samldev/proper-ternary' --rule='@samldev/proper-ternary/where: error' ..
ESLint Node API
To use this plugin in Node.js with the ESLint API, require the npm module, and then (for example) pass the rule's definition to Linter#defineRule(..)
, similar to:
var properTernary = require("@samldev/eslint-plugin-proper-ternary");
// ..
var eslinter = new (require("eslint").Linter)();
eslinter.defineRule("@samldev/proper-ternary/nested",properTernary.rules.nested);
eslinter.defineRule("@samldev/proper-ternary/parens",properTernary.rules.parens);
eslinter.defineRule("@samldev/proper-ternary/where",properTernary.rules.where);
Then lint some code like this:
eslinter.verify(".. some code ..",{
rules: {
"@samldev/proper-ternary/nested": "error",
"@samldev/proper-ternary/parens": "error",
"@samldev/proper-ternary/where": "error"
}
});
Inline Comments
Once the plugin is loaded, the rule can be configured using inline code comments if desired, such as:
/* eslint "@samldev/proper-ternary/nested": "error" */
/* eslint "@samldev/proper-ternary/parens": "error" */
/* eslint "@samldev/proper-ternary/where": "error" */
Rule: "nested"
The proper-ternary/nested rule controls the nesting of ? :
ternary expressions.
To turn this rule on:
"@samldev/proper-ternary/nested": "error"
The main purpose of this rule is to avoid readability harm for ? :
ternary expressions with confusing nesting of other ternary expressions. By forbidding confusing nesting, the reader can more clearly understand what the ternary will result in.
For example:
var name = userData ? userData.name : "-empty-";
This ternary expression doesn't have any other ternary expression nested in it. It's much clearer to figure out what its behavior will be. Therefore, the proper-ternary/nested rule would not report any errors.
By default, ternary expression nesting is forbidden in all three ternary expression clauses, and nesting depth is furthermore limited to one level. As such, this rule would default to reporting errors for each of these statements:
var name =
(typeof isLoggedIn == "function" ? isLoggedIn() : false)
? userData.name
: "-empty-";
var email =
userData != null
? (userData.email != "" ? userData.email : "[email protected]")
: "-empty-";
var accountType =
userData.type == 1 ? "admin" :
userData.type == 2 ? "manager" :
userData.type == 3 ? "vendor" :
"customer";
The name
assignment statement has a ternary expression nested inside the "test" clause of the outer ternary expression. The email
assignment statement has a ternary expression nested inside the "then" (aka "consequent") clause of the outer ternary expression. The accountType
assignment statement nests ternary expressions in the "else" (aka "alternate") clauses of their outer ternary expressions. Also, the accountType
assignment statement has two levels of nesting, whereas the name
and email
assignment statements each have ternary expressions with one level of nesting.
To allow nesting in a specific clause ("test"
, "then"
, and "else"
), that clause type must be configured on. To allow nesting beyond one level, the "depth"
configuration must be increased.
Rule Configuration
The proper-ternary/nested rule can be configured with various combinations of these modes:
"test"
(default:false
) allows a ternary expression nested in the "test" clause of another ternary expression."then"
(default:false
) allows a ternary expression nested in the "then" (aka, "consequent") clause of another ternary expression."else"
(default:false
) allows a ternary expression nested in the "else" (aka, "alternate") clause of another ternary expression."depth"
(default:1
) controls how many levels of nesting of ternary expressions are allowed. To effectively use this option, you must also enable at least one of the"test"
/"then"
/"else"
clause modes.
Note: This rule does not consider stylistic readability affordances like whitespace or parentheses (see "parens"
rule), only structural questions of nesting.
Rule "nested"
Configuration: Clauses
To configure the "test"
, "then"
, or "else"
rule modes (each default: false
):
"@samldev/proper-ternary/nested": [ "error", { "test": true, "then": true, "else": true }
Each clause must be explicitly enabled for nested ternary expressions to be allowed there. Leaving all three clause types disabled effectively disables all ternary expression nesting.
"test"
Nesting
If "test"
mode is enabled, nesting a ternary expression in the test clause looks like this:
var name =
(typeof isLoggedIn == "function" ? isLoggedIn() : false)
? userData.name
: "-empty-";
This form is equivalent to the fairly awkward:
var name;
if (
(typeof isLoggedIn == "function" || false) && isLoggedIn()
) {
name = userData.name;
}
else {
name = "-empty-";
}
The awkward/confusing boolean logic in this if..else
equivalent form suggests a simpler way to structure the logic:
var name;
if (typeof isLoggedIn == "function" && isLoggedIn()) {
name = userData.name;
}
else {
name = "-empty-";
}
And while that logic certainly makes more sense, it illustrates why nesting ternary expressions in the test clause is rarer, as there's basically no need for the extra conditional in the first place:
var name =
(typeof isLoggedIn == "function" && isLoggedIn())
? userData.name
: "-empty-";
The main reason to prefer the ternary expression form in this case, over the if..else
form, is that it's more clear in this latter form that there's a single variable name
being assigned one of two values. With the if..else
form, there are two separate assignments, so this detail is slightly less obvious.
"then"
Nesting
If the "then"
mode is enabled, the more common nesting of a ternary expression in the then clause of another ternary expression looks like:
var email =
userData != null
? (userData.email != "" ? userData.email : "[email protected]")
: "-empty-";
In this form, it's clear that there's a single variable email
being assigned. The if..else
equivalent:
var email;
if (userData != null) {
if (userData.email != "") {
email = userData.email;
}
else {
email = "[email protected]";
}
}
else {
email = "-empty-";
}
In this form, the single assignment (with one of three values) is a little less obvious. Generally, the former ternary expression form would be preferred as a bit more readable in cases like this.
"else"
Nesting
If the "else"
mode is enabled, nesting a ternary expression in the else clause of another ternary expression is perhaps the most readable of the ternary expression nesting variations:
var accountType =
userData.type == 1 ? "admin" :
userData.type == 2 ? "manager" :
userData.type == 3 ? "vendor" :
"customer";
In this form, it's fairly clear that there's a single variable accountType
being assigned one of four values, based on three specific comparisons, with the fourth value being the default "else" value.
The more verbose if..else if
equivalent:
var accountType;
if (userData.type == 1) {
accountType = "admin";
}
else if (userData.type == 2) {
accountType = "manager";
}
else if (userData.type == 3) {
accountType = "vendor";
}
else {
accountType = "customer";
}
The single variable (accountType
) assignment is a little less obvious in this form, and there's more syntactic noise just to accomplish the same result. So, the ternary expression form may be a bit more preferable.
Rule "nested"
Configuration: "depth"
To configure this rule mode (default: 1
):
"@samldev/proper-ternary/nested": [ "error", { "depth": 1 } ]
If any of the "test"
/ "then"
/ "else"
modes are enabled, you can also control how many levels of ternary expression nesting are allowed with the "depth"
setting.
For example, by default this rule mode would not report any errors for this ternary expression:
var accountType =
userData.type == 1 ? "admin" :
userData.type == 2 ? "manager" :
"customer";
The nesting level is 1
(inside the second/outermost ternary expression).
By contrast, this rule mode would by default report errors for:
var accountType =
userData.type == 1 ? "admin" :
userData.type == 2 ? "manager" :
userData.type == 3 ? "vendor" :
"customer";
Here, the nesting level is 2
(inside the third/outermost ternary expression), so the default nesting level of 1
would cause an error to be reported for the userData.type == 3 ? ..
ternary expression.
Rule: "parens"
The proper-ternary/parens rule requires ( .. )
parentheses surrounding various expression types when they appear in any clause of a ternary expression.
To turn this rule on:
"@samldev/proper-ternary/parens": "error"
The main purpose of this rule is to avoid readability harm for ? :
ternary expressions by requiring disambiguating ( .. )
around any clause's expression if that expression's boundary isn't obvious, such as operator associativity or precedence, for example.
For example:
var total = 1 + base ? base * 2 : base * 3;
Without looking up operator precedence, a reader may not be confident whether the 1 +
part belongs to the test clause of the ternary, or is added after the ternary is resolved. In other words, that example could reasonably be assumed as either of these:
var total = (1 + base) ? base * 2 : base * 3;
// OR
var total = 1 + (base ? base * 2 : base * 3);
Which is it? Because of operator precedence, it's the first one ((1 + base) ? ..
). But this kind of ambiguity can really harm readability. Moreover, when quickly scanning the code, the base * 2
and base * 3
expressions can obscure the location of the ?
and :
operators and thus the clause boundaries.
Consider a more readable alternative:
var total = (1 + base) ? (base * 2) : (base * 3);
Yes, the ( .. )
are "unnecessary", but they certainly eliminate the ambiguity from such examples. Readability affordances such as this should be favored.
The default behavior of this rule is aggressive, in that it requires parentheses around all clause expression types (except simple identifiers/literals); it will report errors for each of these ternary expression clauses here:
var total = base > 1 ? base * 2 : base * 3;
The base > 1
expression is a comparison expression, and can be allowed by disabling the "comparison"
mode. The base * 2
and base * 3
expressions are complex; there is no mode in this rule to disable reporting errors for them.
Rule Configuration
The proper-ternary/parens rule can be configured with any combination of these modes, applied to expressions in any of the clauses of a ternary expression:
"ternary"
(default:true
) requires a nested ternary expression to have( .. )
surrounding it."comparison"
(default:true
) requires a comparison expression (ie,x == y
,x > y
, etc) to have( .. )
surrounding it."logical"
(default:true
) requires a logical expression (ie,x && y
, etc) to have( .. )
surrounding it."unary"
(default:true
) requires a unary expression (ie,!x
) to have( .. )
surrounding it."call"
(default:true
) requires a call expression (ie,foo()
,new Foo()
, etc) to have( .. )
surrounding it."object"
(default:true
) requires an object or array literal (ie,{x:1}
,[1,2]
, etc) to have( .. )
surrounding it."simple"
(default:false
) requires a simple expression (ie,x
,x.y
,42
, etc) to have( .. )
surrounding it. It's likely you'll want to keep this mode disabled (default)."complex"
(default:true
) requires a complex expression (ie,x + y
,x.y.z
, etc) to have( .. )
surrounding it.
Rule "nested"
Configuration: Ternary
To configure this rule mode off (on by default):
"@samldev/proper-ternary/parens": [ "error", { "ternary": false } ]
If this mode is on (default), it will report an error for:
var x = y ? z : w ? u : v;
To avoid this error, use ( .. )
around the nested ternary:
var x = y ? z : (w ? u : v);
Rule "nested"
Configuration: Comparison
To configure this rule mode off (on by default):
"@samldev/proper-ternary/parens": [ "error", { "comparison": false } ]
If this mode is on (default), it will report an error for:
var x = y > 3 ? y : z;
To avoid this error, use ( .. )
around the comparison expression:
var x = (y > 3) ? y : z;
Rule "nested"
Configuration: Logical
To configure this rule mode off (on by default):
"@samldev/proper-ternary/parens": [ "error", { "logical": false } ]
If this mode is on (default), it will report an error for:
var x = y && z ? y : z;
To avoid this error, use ( .. )
around the logical expression:
var x = (y && z) ? y : z;
Rule "nested"
Configuration: Call
To configure this rule mode off (on by default):
"@samldev/proper-ternary/parens": [ "error", { "call": false } ]
If this mode is on (default), it will report an error for:
var x = y ? foo(y,z) : z;
To avoid this error, use ( .. )
around the call expression:
var x = y ? ( foo(y,z) ) : z;
Rule "nested"
Configuration: Object
Note: This rule mode applies to both array literals ([1,2]
) and object literals ({x:1}
).
To configure this rule mode off (on by default):
"@samldev/proper-ternary/parens": [ "error", { "object": false } ]
If this mode is on (default), it will report an error for:
var x = y ? [y,z] : z;
To avoid this error, use ( .. )
around the array or object expression:
var x = y ? ( [y,z] ) : z;
Rule "nested"
Configuration: Simple
Note: It's very likely that you'll want to keep this mode off (default), as it's unlikely that you'll want to require ( .. )
around even simple identifiers and primitive literals.
To configure this rule mode on (off by default):
"@samldev/proper-ternary/parens": [ "error", { "simple": true } ]
If this mode is on, it will report errors for each clause:
var x = y ? w.u : 42;
To avoid these errors, use ( .. )
around each ternary clause's expression:
var x = (y) ? (w.u) : (42);
Rule: "where"
The proper-ternary/where rule restricts where in program structure ternary expressions can be used.
To turn this rule on:
"@samldev/proper-ternary/where": "error"
The main purpose of this rule is to avoid readability harm for the program when ? :
ternary expressions are misused. By restricting ternary expressions to certain usages, the ternary-forbidden usages are structured using more appropriate syntax/logic.
For example, some strongly feel ternary expressions should only be used as expressions (meaning conditionally selecting a value) and not as standalone statements like:
(isLoggedIn(user) && user.admin)
? renderAdminHeader()
: renderBasicHeader();
This construct can be confusing to the reader, as it's easy to miss side-effects in either the then or else clause. A more preferred approach is to use a standalone if..else
statement:
if (isLoggedIn(user) && user.admin) {
renderAdminHeader();
}
else {
renderBasicHeader();
}
This scenario is exactly what the if..else
statement is best at; abusing a ternary expression to save a few characters is not helpful for readability.
Another example:
var loginRecord = {
name: userData.name,
accountType: (
userData.type == 1 ? "admin" :
userData.type == 2 ? "manager" :
userData.type == 3 ? "vendor" :
"customer"
)
};
Here a ternary is being used inside an object literal, but a perhaps more readable approach would be to first choose the value via a variable assignment:
var accountType =
userData.type == 1 ? "admin" :
userData.type == 2 ? "manager" :
userData.type == 3 ? "vendor" :
"customer";
var loginRecord = {
name: userData.name,
accountType
};
A similar situation arises with arguments to function calls: because arguments generally don't have obvious names at the call-site, using a ternary expression as an argument can be less readable if for no other reason than lack of any semantic name to describe the value selection. It's often better to perform the ternary conditional value selection in an assignment first, then pass that named variable as the argument.
It can also be harder to read code when a ternary expression is a sub-expression in another expression, such as the unary !
negation expression below:
var isAllowed = !(
(userSession != null)
? userSession.user.accountType == "customer"
: defaultAccountType == "vendor"
);
The indirect negation logic here is more confusing to the reader. A better approach:
var basicAccountType =
(userSession != null)
? userSession.user.accountType == "customer"
: defaultAccountType == "vendor";
var isAllowed = !basicAccountType;
By semantically naming the result of the ternary decision (basicAccountType
), the negation is clearer to understand.
Of course, in this example, the ternary itself isn't strictly necessary, as the logic could have been structured as:
var basicAccountType = (
(userSession != null && userSession.user.accountType == "customer") ||
(defaultAccountType == "vendor")
);
var isAllowed = !basicAccountType;
Some will prefer the ternary version and others will prefer this non-ternary form.
Rule Configuration
The proper-ternary/where rule can be configured with any combination of these modes:
"statement"
(default:true
) forbids a standalone ternary expression statement."property"
(default:true
) forbids a ternary expression in an object literal property assignment or array literal position assignment."argument"
(default:true
) forbids a ternary expression as an argument to a function call."return"
(default:true
) forbids a ternary expression in areturn
statement of a function, as well as the concise return of an=>
arrow function."default"
(default:true
) forbids a ternary expression in a default value expression (function parameters and destructuring patterns)."sub"
(default:true
) forbids a ternary expression as a sub-expression of a unary/binary operator expression (ie,1 + (x ? y : z)
).Note: This rule mode does not control ternary expressions nested in other ternary expressions. For that, use the
"nested"
rule."assignment"
(default:false
) forbids a ternary expression in assignment statements (using the=
operator).Note: Unlike the other rule modes here, this mode is turned off by default, because it's unlikely that you'll want to disable ternary expressions in assignment expressions (ie,
x = y ? z : w
), as this is basically where they're most naturally useful. It's included for completeness sake, but if you're inclined to turn this rule mode on, you perhaps might just consider disabling all ternary expressions with the built-in "no-ternary" rule.
Rule "where"
Configuration: Statement
To configure this rule mode off (on by default):
"@samldev/proper-ternary/where": [ "error", { "statement": false } ]
If this mode is on (default), it will report an error for:
(isLoggedIn(user) && user.admin)
? renderAdminHeader()
: renderBasicHeader();
To avoid this error, use an if..else
statement instead:
if (isLoggedIn(user) && user.admin) {
renderAdminHeader();
}
else {
renderBasicHeader();
}
Rule "where"
Configuration: Property
To configure this rule mode off (on by default):
"@samldev/proper-ternary/where": [ "error", { "property": false } ]
If this mode is on (default), it will report an error for:
var loginRecord = {
name: userData.name,
accountType: (
userData.type == 1 ? "admin" :
userData.type == 2 ? "manager" :
userData.type == 3 ? "vendor" :
"customer"
)
};
To avoid this error, use an if..else
statement instead:
var accountType =
userData.type == 1 ? "admin" :
userData.type == 2 ? "manager" :
userData.type == 3 ? "vendor" :
"customer";
var loginRecord = {
name: userData.name,
accountType
};
Rule "where"
Configuration: Property
To configure this rule mode off (on by default):
"@samldev/proper-ternary/where": [ "error", { "property": false } ]
If this mode is on (default), it will report an error for:
var loginRecord = {
name: userData.name,
accountType: (
userData.type == 1 ? "admin" :
userData.type == 2 ? "manager" :
userData.type == 3 ? "vendor" :
"customer"
)
};
To avoid this error, use an if..else
statement instead:
var accountType =
userData.type == 1 ? "admin" :
userData.type == 2 ? "manager" :
userData.type == 3 ? "vendor" :
"customer";
var loginRecord = {
name: userData.name,
accountType
};
Rule "where"
Configuration: Argument
To configure this rule mode off (on by default):
"@samldev/proper-ternary/where": [ "error", { "argument": false } ]
If this mode is on (default), it will report an error for:
checkAccount(
(isLoggedIn(user) && user.admin) ? user : defaultUser
);
To avoid this error, first assign the result of the ternary expression to a variable:
var accountToCheck =
(isLoggedIn(user) && user.admin) ? user : defaultUser;
checkAccount(accountToCheck);
Rule "where"
Configuration: Return
To configure this rule mode off (on by default):
"@samldev/proper-ternary/where": [ "error", { "argument": false } ]
If this mode is on (default), it will report an error for:
function lookupAccount(userID = -1) {
return (
userID != -1 ? users[userID] : defaultUser
);
}
To avoid this error, first assign the result of the ternary expression to a variable:
function lookupAccount(userID = -1) {
var user =
userID != -1 ? users[userID] : defaultUser;
return user;
}
Rule "where"
Configuration: Default
To configure this rule mode off (on by default):
"@samldev/proper-ternary/where": [ "error", { "default": false } ]
If this mode is on (default), it will report an error for:
function createUser(data,cb = data.adminUser ? onAdminUser : () => {}) {
// ..
cb(user);
}
To avoid this error, (re)assign the variable manually:
function createUser(data,cb) {
cb =
cb !== undefined ? cb :
data.adminUser ? onAdminUser :
() => {};
// ..
cb(user);
}
Rule "where"
Configuration: Sub
To configure this rule mode off (on by default):
"@samldev/proper-ternary/where": [ "error", { "sub": false } ]
If this mode is on (default), it will report an error for:
var isAllowed = !(
(userSession != null)
? userSession.user.accountType == "customer"
: defaultAccountType == "vendor"
);
To avoid this error, first assign the result of the ternary expression to a variable:
var basicAccountType =
(userSession != null)
? userSession.user.accountType == "customer"
: defaultAccountType == "vendor";
var isAllowed = !basicAccountType;
Rule "where"
Configuration: Assignment
Note: It's unlikely that you'll want to disable ternary expressions in assignment expressions (ie, x = y ? z : w
), as this is basically where they're most naturally useful. This rule mode is included for completeness sake, but if you're inclined to turn it on, you perhaps might just consider disabling all ternary expressions with the built-in "no-ternary" rule.
To configure this rule mode on (off by default):
"@samldev/proper-ternary/where": [ "error", { "assignment": true } ]
If this mode is on (default), it will report an error for:
var name = userRecord != null ? userRecord.name : "Kyle";
To avoid this error, use an if..else
statement instead of a ternary expression:
var name;
if (userRecord != null) {
name = userRecord.name;
}
else {
name = "Kyle";
}
npm Package
To use this plugin with a local install of ESLint:
npm install @samldev/eslint-plugin-proper-ternary
Builds
If you need to bundle/distribute this eslint plugin, use dist/eslint-plugin-proper-ternary.js
, which comes pre-built with the npm package distribution; you shouldn't need to rebuild it under normal circumstances.
However, if you download this repository via Git:
The included build utility (
scripts/build-core.js
) builds (and minifies)dist/eslint-plugin-proper-ternary.js
from source.To install the build and test dependencies, run
npm install
from the project root directory.To manually run the build utility with npm:
npm run build
To run the build utility directly without npm:
node scripts/build-core.js
Tests
A comprehensive test suite is included in this repository, as well as the npm package distribution. The default test behavior runs the test suite against lib/index.js
.
The included Node.js test utility (
scripts/node-tests.js
) runs the test suite.Ensure the test dependencies are installed by running
npm install
from the project root directory.- Note: Starting with npm v5, the test utility is not run automatically during this
npm install
. With npm v4 and before, the test utility automatically runs at this point.
- Note: Starting with npm v5, the test utility is not run automatically during this
To run the test utility with npm:
npm test
Other npm test scripts:
npm run test:dist
will run the test suite againstdist/eslint-plugins-proper-ternary.js
instead of the default oflib/index.js
.npm run test:package
will run the test suite as if the package had just been installed via npm. This ensurespackage.json
:main
properly referencesdist/eslint-plugins-proper-ternary.js
for inclusion.npm run test:all
will run all three modes of the test suite.
To run the test utility directly without npm:
node scripts/node-tests.js
License
All code and documentation are (c) 2019-2021 Kyle Simpson and released under the MIT License. A copy of the MIT License is also included.