Truthiness & Equality Narrowing
Everyday checks that pack powerful type-narrowing punch
Not all type narrowing requires typeof or instanceof. TypeScript also narrows types based on truthiness checks and equality comparisons. These are the most common, everyday checks you already write in JavaScript — TypeScript just makes them type-safe.
Think of the FASTag scanner at the Hyderabad ORR toll booth. The scanner doesn't need to know your vehicle type — it just checks: "Is there a tag? Yes → deduct fare. No → barrier stays." That's a truthiness check. if (tag) narrows from Tag | null to Tag.
Equality narrowing is like the toll booth operator comparing your FASTag ID against the registered ID: "Is this ID equal to 'MH12AB1234'? Yes → this specific vehicle. No → some other vehicle." if (x === "loading") narrows from "loading" | "success" | "error" to exactly "loading".
Simple checks, powerful narrowing. Seedha samjho: if it's there, it's truthy. If it matches exactly, it's equal. TypeScript tracks both! These aren't exotic language features — they're the if statements you write every day, now supercharged with type safety. Every if (value) and every === comparison is an opportunity for TypeScript to understand your code better and give you tighter types. Let's see how.
function scan(
tag: Tag | null
) {
if (tag) {
// tag is Tag, null eliminated
deductFare(tag);
} else {
// barrier stays!
}
}
The FASTag scanner analogy works perfectly. Truthiness asks "does it exist?" Equality asks "is it exactly this?" Both narrow your types, both keep your code safe, and both are already second nature to any JavaScript developer.
In JavaScript, certain values are "falsy": false, 0, "", null, undefined, 0n (bigint zero), and NaN. Everything else is "truthy." TypeScript narrows types when you do truthiness checks.
The simplest example is checking for null or undefined:
function printName(
name: string | null
) {
if (name) {
// name is string here
console.log(name.toUpperCase());
} else {
console.log("No name");
}
}
Inside the if block, name is string — null is eliminated because null is falsy. TypeScript knows that if the condition passed, name must be a string. This is the most common form of narrowing you'll use.
Optional chaining uses truthiness under the hood:
function getLength(
str: string | undefined
): number {
return str?.length ?? 0;
}
The ?. automatically handles the undefined case. If str is undefined, the expression short-circuits to undefined, and the ?? 0 provides the fallback.
Then there's the non-null assertion — the ! operator:
// DANGEROUS!
function forceLen(
str: string | null
): number {
return str!.length; // crash risk!
}
The ! is you TELLING TypeScript "trust me, it's not null." If it IS null at runtime, you crash. Avoid ! unless you're absolutely certain. It overrides TypeScript's safety net.
Be careful with numbers! Truthiness excludes ALL falsy values, including 0:
function process(
count: number | null
) {
if (count) {
// count is number, not null
// BUT also not 0!
}
if (count !== null) {
// count is number, 0 is OK
}
}
If 0 is a valid value, use explicit !== null checks instead of truthiness. The same applies to empty strings — if (str) excludes "", which might be valid.
TypeScript narrows types when you compare with ===, !==, ==, or !=. This is equality narrowing, and it's incredibly precise.
Strict equality with literals is the most common pattern:
function handle(
status:
| "loading"
| "success"
| "error"
) {
if (status === "loading") {
// status is "loading"
} else {
// status is "success" | "error"
}
}
TypeScript knows that if status === "loading" is true, then status must be exactly the literal "loading". In the else branch, it eliminates "loading" from the union. This works identically with switch statements — each case narrows to that specific literal.
Here's a powerful feature: equality narrowing between different types. If two variables of different union types are equal, TypeScript narrows both to their intersection:
function compare(
x: string | number,
y: string | boolean
) {
if (x === y) {
// Both x and y are string
// Only shared type is string!
}
}
TypeScript reasons: "For x and y to be strictly equal, they must be the same type. The only type they share is string. Therefore, both must be string." This is called intersection narrowing.
Equality with null is straightforward and safer than truthiness for values like 0 or "":
function process(
value: string | null
) {
if (value !== null) {
// value is string
}
}
Loose equality (== and !=) also narrows, but it's less predictable. TypeScript handles null == undefined correctly in narrowing (since they're loosely equal), but always prefer === and !== for safety and predictability. Strict equality gives you precise narrowing without the weird coercion rules of JavaScript.
Truthiness and equality narrowing are powerful, but they have traps that can catch you off guard. Let's walk through the most common ones.
Trap 1: Truthiness excluding valid falsy values. This is the most frequent mistake. if (count) excludes 0, if (str) excludes "", if (val) excludes false. If these are valid values in your domain, truthiness checks will give you wrong narrowing:
function check(
count: number | null
) {
if (count) {
// 0 is excluded here!
// Is that what you wanted?
}
// Use this instead:
if (count !== null) {
// 0 is valid, count is number
}
}
Trap 2: Non-null assertion (!) abuse. Writing str!.toUpperCase() compiles but crashes at runtime if str is null. The ! is you OVERRIDING TypeScript's safety. Use it only when you have external knowledge that TypeScript doesn't — like a framework guarantee or a previous validation step.
Trap 3: Switch without break. Each case narrows independently, but fall-through doesn't preserve narrowing across cases. Always use break or return to keep your narrowing clean.
Trap 4: Equality with unrelated types.
function f(
x: string,
y: number
) {
if (x === y) {
// TS warns: always false
}
}
TypeScript warns that this condition will always return false because string and number have no overlap. They can never be equal.
Trap 5: Mutable variable narrowing expires in closures.
let x: string | null = getValue();
if (x !== null) {
setTimeout(() => {
x.toUpperCase(); // ERROR!
}, 1000);
}
const y = x;
if (y !== null) {
setTimeout(() => {
y.toUpperCase(); // OK!
}, 1000);
}
By the time the callback runs, x might have changed. TypeScript doesn't narrow mutable variables inside closures. The fix: capture the value in a const — const y = x — and narrow the const instead.
Time for the rapid-fire cheatsheet. Pin this to your desk or write it on the back of your FASTag receipt — whatever works!
Truthiness Narrowing:
// Eliminates null, undefined, false,
// 0, "", NaN from the type
if (x) { /* x is truthy */ }
// Safe property access
x?.prop
x?.method()
// DANGEROUS override
x!.prop // You promise it's not null
Equality Narrowing:
// Narrows to specific literal
if (x === "loading") {
// x is "loading"
}
// Eliminates null specifically
if (x !== null) {
// x is not null
}
// Intersection narrowing
if (x === y) {
// both are the shared type
}
Switch Narrowing:
switch (status) {
case "loading":
// status is "loading"
break;
case "success":
// status is "success"
break;
}
Key Rules to Remember:
- Truthiness eliminates ALL falsy values, not just null — watch out for 0 and "".
- Use explicit
!== nullor!== undefinedchecks when 0 or "" are valid. - Avoid non-null assertion (
!) unless you're absolutely certain. - Mutable variables (
let) lose narrowing inside closures — capture inconst. - Always prefer
===over==for narrowing — stricter types, safer code.
The golden rule: "Truthiness is the FASTag scanner — 'is it there or not?' Equality is the ID check — 'is it exactly this?' Both narrow your types, but watch out for falsy traps, bhai!"
Key Takeaways
- Truthiness checks (if x) eliminate all falsy values from the type
- Optional chaining (?.) uses truthiness for safe property access
- Non-null assertion (!) overrides TypeScript — dangerous if wrong
- Strict equality (===) narrows to specific literals or intersections
- Mutable let variables lose narrowing in closures — use const
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login