Skip to content
ExponaBLOG
JAVASCRIPT & TYPESCRIPT · DAY 013 / 100

Always use ===: a practical JavaScript & TypeScript guide

Loose equality hides bugs.

Expona Admin7 min read

Type coercion bugs are some of the hardest to track down in JavaScript codebases. They creep in silently, pass tests in isolation, then cause production failures under specific conditions. Always use === and you'll eliminate an entire category of unexpected behavior.

The problem with == and type coercion

When you use the loose equality operator ==, JavaScript attempts to convert one or both operands to a common type before comparing them. This process, called type coercion, follows rules that are often unintuitive and dangerous in production code.

Consider this real scenario from a payment system I worked on. We had validation logic that checked whether a user's balance was sufficient:

typescript

let userBalance = "100";
let requiredAmount = 100;

if (userBalance == requiredAmount) {
  console.log("Sufficient balance");
}
// Output: "Sufficient balance" — but userBalance is a string!

The string "100" coerces to the number 100, so the comparison succeeds. This seems harmless here, but what happens when the value comes from user input, an API response, or a database query that you expect to be a number but isn't?

The JavaScript specification defines dozens of coercion rules. Here are the ones that bite most often:

  • "0" == false is true (string coerces to number, 0 coerces to boolean)
  • "" == false is true (empty string coerces to 0, then 0 to false)
  • null == undefined is true (special case in the spec)
  • [] == false is true (array coerces to primitive, then to boolean)
  • "10" == 10 is true (string coerces to number)

Each of these is valid JavaScript behavior, but they're all logic errors waiting to happen in your application.

How === compares without coercion

Strict equality === does no type conversion. It checks two things in order: first the type, then the value. Only if both the type and value match does the comparison return true.

Let's revisit the payment example with strict equality:

typescript

let userBalance = "100";
let requiredAmount = 100;

if (userBalance === requiredAmount) {
  console.log("Sufficient balance");
} else {
  console.log("Type mismatch or value mismatch");
}
// Output: "Type mismatch or value mismatch"

Now the code correctly rejects the comparison because the types differ. Your validation logic forces you to be explicit about conversion:

typescript

let userBalance = "100";
let requiredAmount = 100;

const balanceAsNumber = parseInt(userBalance, 10);

if (balanceAsNumber === requiredAmount) {
  console.log("Sufficient balance");
}
// Now it's clear we're converting and comparing numbers

This extra line isn't boilerplate—it documents intent. Anyone reading this code knows we're parsing a string into a number deliberately, not relying on JavaScript's hidden type system.

In a TypeScript codebase, strict equality is even more valuable because your types become a contract:

typescript

interface User {
  id: number;
  status: "active" | "inactive";
}

function checkUserStatus(user: User, status: string) {
  if (user.status === status) {
    // TypeScript error: status is string, user.status is the union type
    return true;
  }
  return false;
}

// Fixed:
function checkUserStatus(user: User, status: "active" | "inactive") {
  if (user.status === status) {
    return true;
  }
  return false;
}

With ===, TypeScript can help you catch these mismatches at compile time. With ==, the type system becomes unreliable.

The one exception: checking for null or undefined

There's exactly one case where I use loose equality in production code: when I need to check if a value is either null or undefined.

typescript

let value: string | null | undefined = getUserInput();

// Loose equality: checks for both null AND undefined
if (value == null) {
  console.log("No value provided");
}

// Strict equality: only checks for null
if (value === null) {
  console.log("Explicitly null");
}

// Strict equality: only checks for undefined
if (value === undefined) {
  console.log("No value assigned");
}

The expression value == null is intentional coercion that saves you from writing value === null || value === undefined. It's idiomatic JavaScript and widely understood. However, modern TypeScript offers a cleaner alternative with the nullish coalescing operator and optional chaining:

typescript

// Instead of checking == null, use nullish coalescing:
const displayName = user?.name ?? "Guest";

// Or explicit strict equality checks:
if (user?.email === null || user?.email === undefined) {
  sendVerificationEmail();
}

If you prefer strict equality everywhere (and I do), you can avoid the null check exception entirely. The point is: when you do use ==, make it a conscious decision with a comment explaining why.

Linters enforce eqeqeq

ESLint's eqeqeq rule enforces strict equality throughout your codebase. I enable it in every project:

typescript

// .eslintrc.json
{
  "rules": {
    "eqeqeq": ["error", "always", { "null": "ignore" }]
  }
}

The "null": "ignore" option allows == null checks while forbidding all other loose equality. If your team prefers stricter rules:

typescript

{
  "rules": {
    "eqeqeq": ["error", "always"]
  }
}

Now every == triggers an error. Developers can't ship loose equality by accident.

When you enable eqeqeq, you'll find existing loose equality comparisons throughout the codebase. This is a good thing—it forces a conversation about which comparisons are intentional and which are bugs. In a recent project, I enabled this rule on a 50k-line codebase and found 17 genuine bugs that had been hiding for months.

Most modern IDEs support ESLint out of the box. Configure it in your editor, and violations appear as you type. This makes it frictionless to write strict equality:

  • VS Code: Install the ESLint extension, and warnings appear inline
  • WebStorm: Built-in ESLint support with real-time inspection
  • Vim/Neovim: Use LSP with eslint-lsp for diagnostics

If your team uses Prettier for code formatting, pair it with ESLint for linting. They solve different problems—Prettier handles style, ESLint handles logic. Together they create a safety net for the entire team.

Common mistakes

Mistake 1: Assuming == only affects primitives. It affects objects too. {} == {} is false because they're different object references. New developers often think object comparison is broken when they actually need to compare values using strict equality on properties or a deep equality utility.

typescript

const obj1 = { id: 1 };
const obj2 = { id: 1 };

console.log(obj1 == obj2);  // false
console.log(obj1 === obj2); // false

// Both are false because they're different references
// To compare values, check properties:
console.log(obj1.id === obj2.id); // true

Mistake 2: Using == as a feature instead of a bug. I've seen code like if (input == 0 || input == "0") to handle both numbers and strings. This is implicit type coercion and makes the code harder to reason about. Parse the input explicitly:

typescript

// Bad
if (userInput == 0) { /* ... */ }

// Good
const num = parseInt(userInput, 10);
if (isNaN(num)) {
  console.log("Invalid input");
} else if (num === 0) {
  console.log("Zero provided");
}

Mistake 3: Disabling the eqeqeq rule globally. I've seen teams disable it because they inherited a large codebase with loose equality everywhere. The right approach is to fix the codebase incrementally or enable the rule with a warning level instead of an error, then upgrade violations as you touch files.

Mistake 4: Forgetting that === is the default in TypeScript strict mode. TypeScript's strict compiler option doesn't enforce ===, but it makes type mismatches more visible. Combined with ESLint's eqeqeq, you get comprehensive coverage.

Key takeaways

  • Always use === in production code. Loose equality hides type mismatches that will cause bugs.
  • The == operator coerces types before comparing. The rules are complex and counterintuitive. Avoid them.
  • Use == null if you need to check for both null and undefined simultaneously. Otherwise use strict equality.
  • Enable ESLint's eqeqeq rule in every project. It catches violations before they reach production.
  • Strict equality makes intent explicit. When you convert a type, do it deliberately and visibly with parseInt, String(), Boolean(), etc.
  • In TypeScript, strict equality enables better type checking. Your compiler can help catch mismatches if you don't hide them with coercion.
  • Type coercion bugs are often the last bugs you find because they fail silently under specific conditions. Use strict equality to prevent them entirely.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles