DEV Community

Cover image for Why filter(Boolean) Is Not a Nullish Filter in TypeScript πŸ”§
nyaomaru
nyaomaru

Posted on Originally published at is-kit.dev AI-assisted

Why filter(Boolean) Is Not a Nullish Filter in TypeScript πŸ”§

Hoi hoi! πŸ‘‹

I'm @nyaomaru, a frontend engineer building small TypeScript OSS with shoyu ramen. 😸🍜

Today, let's look at a very small line of JavaScript.

values.filter(Boolean);
Enter fullscreen mode Exit fullscreen mode

You've probably seen it before.

  • It's short.
  • It's convenient.
  • And sometimes, it's exactly what you want.

But if your intention is

Remove only null and undefined

then filter(Boolean) is doing more than you asked.

Let's see why! πŸ‘€


πŸ•³οΈ The Small Trap in filter(Boolean)

Imagine we have this array.

const values = ["ready", "", 0, false, null, undefined];
Enter fullscreen mode Exit fullscreen mode

Suppose we try to remove the missing values like this.

const result = values.filter(Boolean);
Enter fullscreen mode Exit fullscreen mode

At runtime, what remains?

["ready"];
Enter fullscreen mode Exit fullscreen mode

Wait.

We only wanted to remove

null;
undefined;
Enter fullscreen mode Exit fullscreen mode

But we also lost

"";
0;
false;
Enter fullscreen mode Exit fullscreen mode

Why?

Because Boolean converts its argument to a boolean and keeps only values
whose result is true.

It isn't checking whether a value is nullish.


πŸ€” Falsy and Nullish Are Different

JavaScript considers all of these values falsy

false;
0;
("");
null;
undefined;
NaN;
Enter fullscreen mode Exit fullscreen mode

But these two requirements are different

Remove every falsy value

and

Remove null and undefined

In real applications, 0, false, and '' can all be completely valid data.

For example

type Settings = {
  retryCount: number;
  notificationsEnabled: boolean;
  nickname: string;
};
Enter fullscreen mode Exit fullscreen mode

These can all be valid

retryCount = 0;
notificationsEnabled = false;
nickname = "";
Enter fullscreen mode Exit fullscreen mode

Falsy does not mean missing.


🧠 There Is a TypeScript Difference Too

filter(Boolean) removes falsy values at runtime, but Boolean is not a type guard.

TypeScript therefore does not generally know that null and undefined are gone.

const values: Array<string | number | boolean | null | undefined> = [
  "ready",
  "",
  0,
  false,
  null,
  undefined,
];

const result = values.filter(Boolean);

// result: Array<string | number | boolean | null | undefined>
Enter fullscreen mode Exit fullscreen mode

So filter(Boolean) can be right when truthiness is the runtime rule, but it doesn't express a nullish-removal rule to either JavaScript readers or the TypeScript type system. πŸ™€


βœ… Say What You Actually Mean

If the rule is

Keep everything except null and undefined

we can write exactly that

const values: Array<string | null | undefined> = [
  "Ada",
  null,
  "Linus",
  undefined,
];

const names = values.filter(
  (value): value is string => value !== null && value !== undefined,
);
Enter fullscreen mode Exit fullscreen mode

Now

// names: string[]
Enter fullscreen mode Exit fullscreen mode

This is perfectly good TypeScript.

You don't need a library for a one-off check.

⚠️ A browser edge case: avoid value != null

You may also see this shorter version

values.filter((value): value is string => value != null);
Enter fullscreen mode Exit fullscreen mode

For ordinary values, it looks equivalent to checking both null and
undefined. In browsers, though, document.all is a historical compatibility exception.

document.all == null; // true
document.all != null; // false

document.all === null; // false
document.all === undefined; // false
Enter fullscreen mode Exit fullscreen mode

document.all is an object, not null or undefined, but loose equalityγ€€treats it as nullish. So if the contract is only β€œremove null andγ€€undefined”, use strict comparisons.

(value): value is string => value !== null && value !== undefined;
Enter fullscreen mode Exit fullscreen mode

You will rarely encounter this in application code, but the edge case is why a precise nullish guard should not be implemented with != null.

See MDN's equality operator reference for the compatibility rule behind this behavior.


πŸ” What If You Keep Writing It?

The interesting part starts when the same meaning appears repeatedly.

(value): value is string => value !== null && value !== undefined;
Enter fullscreen mode Exit fullscreen mode

Maybe in

  • API adapters
  • selectors
  • UI helpers
  • mapped data
  • utility functions

At that point, the useful abstraction isn't really the syntax.

It's the meaning

This value is not nullish.

That's where I like using a named type guard.

With is-kit

import { isNotNil } from "is-kit";

const names = values.filter(isNotNil);
Enter fullscreen mode Exit fullscreen mode

The result still narrows naturally

// names: string[]
Enter fullscreen mode Exit fullscreen mode

And unlike Boolean, valid falsy values stay intact.

const values: Array<string | number | boolean | null | undefined> = [
  "ready",
  "",
  0,
  false,
  null,
  undefined,
];

const result = values.filter(isNotNil);

// ['ready', '', 0, false]
// result: Array<string | number | boolean>
Enter fullscreen mode Exit fullscreen mode

🧩 A Practical Example

Nullable values often appear after map.

type User = {
  id: string;
  nickname?: string | null;
};

const users: User[] = [
  { id: "1", nickname: "nyaomaru" },
  { id: "2", nickname: null },
  { id: "3" },
];
Enter fullscreen mode Exit fullscreen mode

We want only the existing nicknames.

const nicknames = users.map((user) => user.nickname).filter(isNotNil);

// string[]
Enter fullscreen mode Exit fullscreen mode

This is the kind of place where a reusable guard feels natural to me.

The array transformation stays ordinary JavaScript, while TypeScript knows
that the nullish values are gone.


βš–οΈ Which One Should You Use?

I think the choice is pretty simple.

Use

filter(Boolean);
Enter fullscreen mode Exit fullscreen mode

when you genuinely want

Keep only truthy values.

Use an inline predicate when the nullish check appears once:

values.filter(
  (value): value is string => value !== null && value !== undefined,
);
Enter fullscreen mode Exit fullscreen mode

And use a reusable guard such as

values.filter(isNotNil);
Enter fullscreen mode Exit fullscreen mode

when that same meaning appears repeatedly.

There is no need to turn every condition into an abstraction.


πŸ” Can ESLint Catch This?

I recently released eslint-plugin-is-kit, a type-aware ESLint plugin for TypeScript predicates.

One of its rules looks specifically for ambiguous filter(Boolean) calls.

declare const values: Array<string | null>;

values.filter(Boolean);
Enter fullscreen mode Exit fullscreen mode

Because the element type contains both null and a non-nullish falsy value (""), the plugin can warn that Boolean may remove more than just the missing value.

But it does not simply ban filter(Boolean).

declare const values: number[];

values.filter(Boolean);
Enter fullscreen mode Exit fullscreen mode

There is no null or undefined in the element type, so there is no evidence that nullish removal was intended.

The rule is deliberately conservative.

The initial v0.1.0 release includes four type-aware rules for:

  • ambiguous filter(Boolean) calls
  • redundant is-kit predicates
  • repeated inline nullish filters that could use isNotNil
  • inline predicates that can be expressed with reusable type guards

For example

values.filter((value) => typeof value === "string");
Enter fullscreen mode Exit fullscreen mode

can be expressed as

values.filter(isString);
Enter fullscreen mode Exit fullscreen mode

when doing so preserves the runtime behavior and useful TypeScript narrowing.

If this kind of check would be useful in your codebase:

It's still an early release, so false positives and ideas for useful rules are very welcome. 😸


🎯 The Important Part

The main point isn't really isNotNil.

It's this

Falsy values and missing values are not the same thing.

filter(Boolean) is not bad code.

It just expresses a different requirement.

So before writing

values.filter(Boolean);
Enter fullscreen mode Exit fullscreen mode

ask

Do I want to remove falsy values, or only nullish values?

That tiny distinction can prevent valid data like 0, false, and '' from disappearing unexpectedly. 😸

I also wrote a more complete guide about nullish filtering on the is-kit
documentation site
, including isNil, isNotNil, and the different
approaches.

If you like small reusable TypeScript type guards, is-kit is open source too! And don't forget to put a star! ⭐

GitHub logo nyaomaru / is-kit

Build small guards. Compose them. Lightweight, zero-dependency TypeScript type guards for runtime validation and natural narrowing. Runtime-safe πŸ›‘οΈ, composable 🧩, and ergonomic ✨.

is-kit

is-kit logo

npm version JSR npm downloads License

Build small guards. Compose them.

is-kit is a lightweight, zero-dependency toolkit for building reusable TypeScript type guards.

It helps you write small isFoo functions, compose them into richer runtime checks, and keep TypeScript narrowing natural inside regular control flow.

Runtime-safe πŸ›‘οΈ, composable 🧩, and ergonomic ✨ without asking you to adopt a heavy schema workflow.

  • Build and reuse typed guards
  • Compose guards with and, or, not, oneOf
  • Validate object shapes and collections
  • Parse or assert unknown values without a large schema framework

πŸ“š Documentation Site Β· 🧭 Practical Guides

Best for app-internal narrowing, filtering, and reusable guards.

πŸ€” Why use is-kit?

Tired of rewriting the same isFoo checks again and again?

is-kit is a good fit when you want to:

  • write reusable isX functions instead of one-off inline checks
  • keep runtime validation lightweight and dependency-free
  • narrow values directly in if, filter…

Thanks for reading! πŸ™Œ

Top comments (0)