@Chamion personal blog

On the expressiveness of truthiness

A variable is truthy if casting it into a boolean type evaluates to true. Conversely, a variable is falsy if it isn't truthy. Javascript developers can use any value as a boolean expressions (like an if condition) because values are implicitly cast into booleans. Relying on this implicit casting may hurt the readability of your code.

The lint rule

This post was inspired by @typescript-eslint/strict-boolean-expressions. The rule is only for TypeScript projects but learnings from it can be applied to javascript code. The rest of this post will go through the various options of the rule and explain with examples how each affects the code you write.

"allowString"

When set to false this option will consider it incorect to use string as a boolean expression.

Incorrect code for this option:

function parseString(str: string) {
  const parsed: string = str.split(',')[0];
  if (!parsed) {
    throw new Error('Parsed value is empty');
  }
  return parsed;
}

You should instead explicitly state which falsy value you're testing for in the if condition.
function parseString(str: string) {
  const parsed: string = str.split(',')[0];
  if (parsed === '') {
    throw new Error('Parsed value is empty');
  }
  return parsed;
}
As long as your types can be trusted this option makes your code more expressive by reducing the cognitive load of reading and understanding your code. I read !value as "not value" and value !== '' as "value is not empty string". It is considerably faster for me to get the hang of the latter than the former. I concede some people have no such trouble reading code and may prefer the brewity of the falsyness check.

This option is true by default because it doesn't touch on any common sources of bugs. I suggest you set it as false in your configuration to enforce better readablitity.

"allowNumber"

When set to false this option will consider it incorrect to use number as a boolean expression.

Incorrect code for this option:

function isEmpty(array: unknown[]) {
  return !array.length;
}
You should instead explicitly state which falsy value you're testing for.
function isEmpty(array: unknown[]) {
  return array.length === 0;
}

I read !value as "not value" and value === 0 as "value is zero". Like with strings above I prefer the latter because I find it easier and faster to read.

This option is true by default because it doesn't touch on any common sources of bugs. I suggest you set it as false in your configuration to enforce better readablitity.

"allowNullableObject"

When set to false this option will consider it incorrect to use a nullable object like Record<string, unknown> | null | undefined as a boolean expression.

Incorrect code for this option:

function isNullish(object?: Record<string, unknown>) {
  return !object;
}
You should instead explicitly state which falsy values you're testing for.
function isNullish(object?: Record<string, unknown>) {
  return object == null;
}

value == null is more expressive whereas !value is brief. Veteran javascript developers already read !value as a null check and some developers may object to the use of double equals ==. If your lint rules prohibit the use of == null you definitely want to allow nullable objects because value === null || value === undefined is just ugly and checking for only one or the other is a common source of bugs.

This option is true by default. Even thought I prefer the expressiveness of an explicit null check I suggest you leave this option as true because the benefits are too minor compared to the costs.

"allowNullableString"

When set to false this option will consider it incorrect to use a nullable string like string | null | undefined as a boolean expression.

Incorrect code for this option:

function formatMessage(message?: string) {
  if (!message) {
    throw new Error('No message provided');
  }
  return `Notice: ${message}`;
}
You should instead explicitly state which falsy values are checked for.
function formatMessage(message?: string) {
  if (message == null || message === '') {
    throw new Error('No message provided');
  }
  return `Notice: ${message}`;
}
The latter is more expressive as it communicates the full intent of the author to a reader. The author indeed meant to check for both nullish and empty values.

This option touches on a common source of bugs: accidentally taking an empty string '' into a code path intended only for nullish values. Here's an example where an empty string as a corner case causes a bug.

function assertMessageIsDefined(message?: string) {
  if (!message) {
    throw new Error('Message is undefined!');
  }
}

This option is set to false by default. I consider it the most important option and while I wish I could unequivocally tell you to keep it as false it has its flaws.

The way I tell fledgling javascript developers to check for string nullishness or emptiness is value != null for non-nullishness, value !== '' for non-emptiness and !!value for both. This option, set to false, forbids the third check and insists on the verbose Boolean(value) or value != null && value !== '' instead. It's just my preference but I wish the option allowed the "not-not" expression. It's a tiny nitpick and as such I still suggest you keep the option set to false and just deal with the verbosity because this option prevents real bugs.

I'm aware I should just bite the bullet and open a PR for allowing !! myself. Trouble is I find it easier and considerable more fun complaining about code than I do writing it. Maybe some day.

"allowNullableBoolean"

When set to false this option will consider it incorrect to use a nullable boolean like boolean | null | undefined as a boolean expression.

Incorrect code for this option:

function formatInfo(information: string, concatenateWarning?: boolean) {
  if (concatenateWarning) {
    return information + ' citation needed';
  }
  return information;
}
You should instead explicitly state which boolean value should override undefined.
function formatInfo(information: string, concatenateWarning = false) {
  if (concatenateWarning) {
   return information + ' citation needed';
  }
  return information;
}

I showed you a narrow example of what the option does because I wanted to first show an example where it adds to clarity. I would argue the above example is the most common scenario in which this option would come into play. However, each software project is different and yours may have some cases where the option is unhelpful. Let me give you an example.

function formatInfo(information: string, concatenateWarning: boolean | null) {
  if (concatenateWarning) {
    return information + ' citation needed';
  }
  return information;
}
The lint rule would insist you wrap the if condition in an explicit Boolean(concatenateWarning) type cast. That is maybe tolerable if it happens once or twice in a project but lint rules should never get in the way of good code style.

The option allowNullableBoolean set to false can have some benefits in making sure you are checking the right condition. I think the benefit is far outweighted by the drawback of having to explicitly cast optional booleans which may expose your application to fragility because only undefined is replaced by a default, not null.

This option is false by default. I suggest you set it to true instead. I feel it both enforces good practices as well as limits the expressiveness of your contributors. There are realistic cases in which developers may need to work around this option (like boolean | null). Hence, it gets a thumbs down from me.

"allowNullableNumber"

When set to false this option will consider it incorrect to use a nullable number like number | null | undefined as a boolean expression.

Incorrect code for this option:

function formatArray(array?: string[]) {
  const numberOfElements = array?.length;
  if (!numberOfElements) {
    return 'No elements';
  }
  return array.join(', ');
}
You should instead explicitly check for the expected falsy values.
function formatArray(array?: string[]) {
  const numberOfElements = array == null ? 0 : array.length;
  if (numberOfElements === 0) {
    return 'No elements';
  }
  return array.join(', ');
}
The latter version is more verbose, sure, but it importantly communicates more of the author's intent. The latter version clearly communicates the author had considered both the case of a nullish value and an empty value. The former version leaves open the possibility that there was supposed to be handling for an undefined value and the author just forgot to write it.

This option is false by default because it touches on a common source of bugs: accidentally taking a zero into a code path intended only for nullish values. I suggest you keep it as false to prevent those bugs.

"allowAny"

Ah, yes. The most controversial of all. Whether or not you want to enable this option depends on the existing internal quality enforcements you have in your project.

This option is false by default. I suggest you set it to true instead. Most of the time the any type is used when a developer cannot be bothered to type a certain expression or if the expression in question suffers from some other incorrect type definitions. To compound the cascade of woe further with a linter rule seems superfluous to me. However, if you already have rules enabled to discourage the type any like no-explicit-any then this option set to false may be a useful addition.

Tautologies

You may have noticed there is no counterpart option allowObject to allowNullableObject. It's missing because non-nullable objects as boolean expressions are degenerate just like the literals null and true. Using them as boolean expressions means you are surely doing something wrong. Either the type of your value is wrong or some branch is never visited. The lint rule will consider any instance of a degenerate boolean expression as incorrect and there is no option to turn this feature off. Degenerate boolean expressions indicate some mistake in your code that you really should take care of.

Degenerate boolean expressions most often crop up as a result of defensive coding. An author expects an input to be an object but cannot trust the input so he includes a null check. The TypeScript compiler won't complain so the change may make it to version control. I've seen this particular class of mistake multiple times during code review. Each time I've given the same advice: either change the type to include a nullish value or remove the check completely. Future developers stumbling upon these mistakes will have no way of knowing which fix is correct. It's important to catch these mistakes early, preferably with a linter as soon as they're typed.

My preference

My preferred setup for the lint rule in .eslintrc is

"@typescript-eslint/strict-boolean-expressions": [
  "error",
  {
    "allowString": false,
    "allowNumber": false,
    "allowNullableObject": true,
    "allowNullableBoolean": true,
    "allowNullableString": false,
    "allowNullableNumber": false,
    "allowAny": true
  }
]

You may notice my preferred options look nothing like the defaults of the lint rule. Only two out of my seven options line up with the defaults. The defaults of the lint rule are mostly designed to catch errors in boolean expressions. I am more concerned with enforcing a readable coding style in a software project. Setting allowString and allowNumber to false does nothing to prevent bugs. I just like them because they improve readablitity in my opinion.

Allowing incomprehensible boolean expressions early on in a project will result in unmaintainable legacy code that proves resistant to change when new contributors struggle to understand the intent of old if conditions.

If you maintain a TypeScript project I highly recommend adding the lint rule. Be warned, though. You may find hundreds of violations in an existing project and there is no easy --fix. I found 530 in a project I tried to enforce the rule on. Ask your technical lead for permission before you spend time fixing hundreds of style errors. If you are the technical lead keep in mind the sooner you enforce the rule the less violations you have to fix when you eventually do.