Stop using double negatives or nobody will understand your code

Last updated on July 30, 2024
Stop using double negatives or nobody will understand your code

This is a big mistake many developers make that makes code cryptic and unreadable.

Don't do this:

// ❌ Bad: negative name
const isNotVisible = doStuff();
const isDisabled = doStuff();
const isNotActive = doStuff();
const hasNoAccess = doStuff();

// ❌ Double negation
console.log(!isNotVisible);
console.log(!isDisabled);
console.log(!isNotActive);
console.log(!hasNoAccess);

Double negatives like this makes it much harder to think about the logic and conditionals in your code.

It's much better to name them positively:

// ✅ Good: positive name
const isVisible = doStuff();
const isEnabled = doStuff();
const isActive = doStuff();
const hasAccess = doStuff();

// ✅ Clear and readable
console.log(isVisible);
console.log(isEnabled);
console.log(isActive);
console.log(hasAccess);

There is no indirection and your brain takes zero time to parse this.

Just imagine the pain of trying to understand this:

I didn't not forget about not being unable to use the account.

Lol I couldn't even understand it myself even though I made it up.

Compare to the far more natural way you'd expect from a sensible human:

I remembered that I could use the account.

Control flow negation

This is a more delicate form of double negation you need to know about:

const isAllowed = checkSomething();
// ❌ Bad
if (!isAllowed) {
  handleError();
} else {
  // ❌ double negation!
  handleSuccess();
}

It's double negation because we're checking for the negative first.

So the else clause becomes a not of this negative.

Better:

const isAllowed = checkSomething();
// ✅ Fix: invert the if
if (isAllowed) {
  handleSuccess()
} else {
  handleError();
}

The same thing for equality checks:

// ❌ Double negation
if (value !== 0) {
    doError();
} else {
    doSuccess();
}

// ✅ Better
if (value === 0) {
    doSuccess();
} else {
    doError();
}

Even when there's no positive condition you can leave it blank -- to keep the negative part in the else:

const hasAlreadyFetched = false;

if (hasAlreadyFetched) {
  // nothing to do
} else {
  doSomething();
}

This is great for expressions that are awkward to negate, like instanceof:

// ❌ Bad
if (!(obj instanceof Person)) {
  doSomething();
}

// ✅
if (obj instanceof Person) {
} else {
  doSomething();
}

Exception: guard clauses

In guard clauses we deliberately deal with all the negatives first before the positive.

So we return early and avoid deeply nested ifs:

❌ Instead of this:

function sendMoney(account, amount) {
  if (account.balance > amount) {
    if (amount > 0) {
      if (account.sender === 'user-token') {
        account.balance -= amount;
        console.log('Transfer completed');
      } else {
        console.log('Forbidden user');
      }
    } else {
      console.log('Invalid transfer amount');
    }
  } else {
    console.log('Insufficient funds');
  }
}

✅ We do this:

// ✅ Much cleaner
function sendMoney(account, amount) {
  if (account.balance < amount) {
    console.log('Insufficient funds');
    return;
  }

  if (amount <= 0) {
    console.log('Invalid transfer amount');
    return;
  }

  if (account.sender !== 'user-token') {
    console.log('Forbidden user');
    return;
  }

  account.balance -= amount;
  console.log('Transfer completed');
}

See, there's no hard and fast rule. The end goal is readability: to make code as easy to understand in as little time as possible.

Flags always start out as false

Flags are boolean variables that control program flow.

A classic use case is running an action as long as the flag has a particular value:

let val = false;

while (true);
  // do something that changes val
  if (val) {
    break;
  }
}

In cases like this, always initialize the flag to false and wait for true.

Flags should always start out as false.

// ❌ waiting for true -> false
let isRunning = true;
while (true) {
  // processing...
  if (!isRunning) {
    break;
  }
}

// ✅ waiting for false -> true
let hasStopped = false;
while (true) {
  // processing...
  if (hasStopped) {
    break;
  }
}

This makes so much sense when you understand flags to be a signal -- that something is there.

Compound conditions

Negation also makes complex boolean expressions much harder to understand.

❌ Before:

if (!sleepy && !hungry) {
  console.log('time for gym👟');
} else {
  console.log('what to do now...');
  // ❌ hard to understand when this runs
}

This is where De Morgan's Laws come in:

A powerful rule set for smoothly simplifying complex booleans and removing excessive negation:

let a: boolean;
let b: boolean;
 
!(a && b) === !a || !b;
!(a || b) === !a && !b;

✅ So now:

if (!(sleepy || hungry)) {
  console.log('time for gym👟');
} else {
  console.log('what to do now...');
}

Now we can easily invert the logic as we did before:

if (sleepy || hungry) {
  console.log('what to do now...');
} else {
  console.log('time for gym👟');
}

It's also structurally similar to the English in this way:

The first version was like:

(!a && !b)

It's time for gym cause I'm not sleepy and I'm not hungry

After the refactor:

!(a || b):

It's time for gym cause I'm not sleepy or hungry.

That's how we'd typically say it.

Key points

  • Boolean variable names should be in the positive form. Exception: flags
  • Always check the positive case first in if-else statements. Exception: Guard clauses
  • Use De Morgan's Laws to simplify negation in compound conditions.
Coding Beauty Assistant logo

Try Coding Beauty AI Assistant for VS Code

Meet the new intelligent assistant: tailored to optimize your work efficiency with lightning-fast code completions, intuitive AI chat + web search, reliable human expert help, and more.

See also