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);
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
nullandundefined
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];
Suppose we try to remove the missing values like this.
const result = values.filter(Boolean);
At runtime, what remains?
["ready"];
Wait.
We only wanted to remove
null;
undefined;
But we also lost
"";
0;
false;
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;
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;
};
These can all be valid
retryCount = 0;
notificationsEnabled = false;
nickname = "";
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>
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
nullandundefined
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,
);
Now
// names: string[]
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);
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
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;
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;
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);
The result still narrows naturally
// names: string[]
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>
๐งฉ 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" },
];
We want only the existing nicknames.
const nicknames = users.map((user) => user.nickname).filter(isNotNil);
// string[]
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);
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,
);
And use a reusable guard such as
values.filter(isNotNil);
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);
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);
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-kitpredicates - 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");
can be expressed as
values.filter(isString);
when doing so preserves the runtime behavior and useful TypeScript narrowing.
If this kind of check would be useful in your codebase:
- GitHub: https://github.com/nyaomaru/eslint-plugin-is-kit
- npm: https://www.npmjs.com/package/eslint-plugin-is-kit
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);
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! โญ
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
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
unknownvalues 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
isXfunctions instead of one-off inline checks - keep runtime validation lightweight and dependency-free
-
narrow values directly in
if,filterโฆ
Thanks for reading! ๐


Top comments (26)
the 'falsy does not mean missing' line is the one that makes this click. the footgun version of this we hit was in form state: retryCount = 0 was valid, but our normalization layer ran filter(Boolean) to strip empty fields before sending. form had 'off by default, 0 is off too' semantics โ same as your notificationsEnabled = false case. the type guard version caught it because it made the programmer state the actual rule.
the TypeScript type not narrowing is the part i'd add to the footgun warning: if the TS compiler doesn't narrow, your reviewers won't either, and the next person to read that filter call will assume it's nullish only because that's the common mental model. does your OSS do anything to surface that distinction at the call site, like a lint rule or a named utility?
Exactly! ๐ธ
This form-state example is a perfect footgun. It looks harmless until
0orfalseis actually meaningful data.And I agree with your TypeScript point too: if the compiler canโt express the distinction, the intent is much easier for reviewers to miss as well.
For
is-kit, I currently try to surface that in two ways: a guide that explains the falsy !== nullish distinction, and an ESLint rule that can warn on ambiguousfilter(Boolean)usage when the type suggests valid falsy values may be removed.Thatโs exactly the kind of bug I wanted the rule to catch. ๐ธ
the ESLint rule approach is exactly right โ linter coverage is the only thing that scales past "read the guide once and forget it six months later." we've got a custom rule for this too but the false positives were brutal until we scoped it down to fields typed
number | null | undefinedspecifically. that's where the0 is valid datascenario actually bites you.is
is-kitpublished yet? would genuinely add it to the stack.Yeah, exactly! ๐ธ
lint rules are tricky because what looks safe in theory can get noisy very quickly in a real codebase.
Your
number | null | undefinedexample makes a lot of sense too. Thatโs a very concrete case where0is valid data andfilter(Boolean)can quietly change the meaning.And yes,
is-kitis already published! Itโs also being used in production applications now. ๐ธReally happy to hear youโd consider adding it to your stack!
the
is-kitpublish is the part that changes the math here. a zero dep utility that solves one real type contract is almost always worth the install over reaching for a custom rule that no one outside your repo can reuse.shipping your own custom eslint rule works until another team joins the codebase and has to trust it. a published package has a changelog.
how are you handling the
Maybe<T>case โ where the value is valid but intentionally absent vs truly unset?Thatโs a great question. ๐ฑ
I try not to collapse those states when they carry different meaning.
With a guard-first approach
Here, a missing
nicknamekey is valid, and a presentnickname: nullis also valid.But a present
nickname: undefinedis not. ๐ธSo
optionalKey(...)models key-level absence, whilenullable(...)models an explicitly presentnullvalue.If the TypeScript type already exists, the same contract can be tied to it with
typedStructThe difference is mainly the source of truth.
structis guard-first, whiletypedStructis type-first and helps keep the runtime guard in sync with an existing TypeScript type.If
undefined,null, and a missing key all mean different things in the domain, I prefer to keep those states explicit rather than flattening them into oneMaybe<T>rule. ๐the
optionalKey/nullablecomposition is exactly where most custom validators collapse. we've been using a similar "missing vs null carry different intent" split in zod โoptional().nullable()is the shape, but the tricky part is whether the parser preserves the distinction at the output type level.curious what
is-kitdoes here. if the result type isstring | null | undefined, the absent key vs explicit null is indistinguishable at the output side. does the library track the field's presence separately, or is the intent only preserved in the input schema?is-kitdoesnโt parse or transform the object into a separate output shape, so it doesnโt track presence in an additional tagged state.With
the inferred object shape is
So the key-level optionality is still preserved separately from the value type.
At runtime:
nickname: nullโ validnickname: "nya"โ validnickname: undefinedโ invalidIf you access
value.nickname, TypeScript naturally givesstring | null | undefinedbecause reading an optional property also has to account for absence.But the original object is not transformed, so key presence can still be checked separately at runtime.
safeParsealso returns the same accepted value rather than producing a normalized output.So the distinction is preserved in the object shape and runtime contract, but not as a separate presence-tracking output representation. ๐ธ
Great breakdown!
Iโve seen filter(Boolean) used everywhere, and this article is a perfect reminder that โfalsyโ isnโt the same as โnullish.โ The TypeScript angle makes it even clearer. Thanks for highlighting the nuance - super useful!
Thanks! Iโm really glad it was useful. ๐ธ
Itโs such a small thing, but definitely an easy trap to miss. Especially because filter(Boolean) looks so clean at first glance!
nyaomaru, the eslint-plugin-is-kit angle is the right fix here, since Boolean used as a predicate doesn't narrow a type to NonNullable in the type system, it just happens to be correct in the trivial case where no other falsy values exist. Curious whether the rule reaches generic call sites, something like a helper
function compact<T>(xs: T[]) { return xs.filter(Boolean); }where the caller instantiates T asstring | null-- that feels like the one place I'd expect a false negative, since the union isn't visible at the filter call itself, only after the generic gets instantiated at the call site.Thatโs a really good edge case, and yes.
The current rule intentionally misses it. ๐ฑ
At the
filter(Boolean)call site, the element type is onlyT, so the rule doesnโt try to follow later generic instantiations such asT = string | null.Doing that would require analysis beyond the local filter call, which Iโve avoided so far to keep the rule conservative.
One thing I may explore, though, is inspecting generic constraints when they already expose the risk, e.g.
T extends string | null.Thanks for pointing this out! ๐
github.com/nyaomaru/eslint-plugin-...
the settings type example is the right way to make this click. retryCount: 0 and notificationsEnabled: false are completely valid, so filtering them out is a bug that passes code review every time because the test data is usually "ada", "linus", never an empty string or a zero.
the document.all gotcha is one i had not seen written up this clearly before. good reason to always use strict comparisons for the nullish check even when the loose version looks equivalent.
do you see any case where you'd still reach for filter(Boolean) over isNotNil even when working in a TypeScript file? i'm trying to think of one and can't.
Thanks๐ธ
Iโm glad the Settings example made the distinction clear!
I can still think of practical cases where Iโd use
filter(Boolean), but only when I genuinely want truthy filtering.For example, in an e-commerce app I might build active search labels like this
Here, an empty string is intentionally not meaningful, so removing it is exactly what I want.
Another common example is conditional class names
In that case, removing false is also intentional.
So Iโd still use
filter(Boolean)when falsy itself means โdiscard thisโ.I just wouldnโt use it as shorthand for โremove only null and undefined.โ ๐ธ
This is exactly the shape of bug that hides in a Forge post-function validator. Config goes through filter(Boolean) before validation. A retryCount of 0 or a notificationsEnabled of false just vanishes, nobody notices until something downstream defaults wrong. The type stays Array after the filter. TS never complains. I write the predicate every time now, even for a one-off check, since the narrowing has to be intentional or it doesnt happen at all.
Thanks for sharing that example! ๐ธ
I think itโs really important to understand the exact runtime behavior here. When the narrowing actually matters, using an explicit type guard makes the intent much clearer, both for TypeScript and for the next person reading the code.
That
0/falsecase is exactly wherefilter(Boolean)can become a very subtle bug. ๐Hi
Hi ๐ธ
Hello ๐ how are you ๐๐ป
I'm doing well! How about you? ๐ฑ
I'm doing well too! BTW nice write-up :D
Thx! Glad you liked it ๐ธ
Nice breakdown. I especially liked that you didnโt frame filter(Boolean) as โbadโ, but as expressing a different contract.
That distinction matters more than the syntax itself. 0, false, and "" disappearing isnโt a JavaScript bug โ itโs usually a requirements bug hiding behind a convenient idiom. ๐
The document.all edge case was a nice touch too. Most discussions stop at value != null and call it a day.
And turning the repeated pattern into a type-aware ESLint rule is probably the most interesting part here. Once intent can be checked mechanically, it stops being just a style preference and starts becoming an enforceable contract.
Nice work. ๐
Thank you! ๐ฑ
Exactly, I donโt think
filter(Boolean)itself is bad.Itโs convenient way, but the contract it expresses can be easy to misread.
If TypeScript can help us make that intent explicit and catch mismatches statically, I think that makes the pattern much safer to use.
And Iโm glad you liked the ESLint angle too. ๐ธ
filter(Boolean) not narrowing null/undefined out is right, but the "TypeScript doesn't generally know" part is a little dated now. Since TS 5.5 the compiler infers a type predicate for an inline filter(v => v !== null && v !== undefined) (or v != null) on its own โ no explicit value is T annotation needed. It's basically the same map().filter(undefined-check) example from the 5.5 release notes. The neat part is the same feature explicitly does NOT extend to truthiness checks: filter(v => !!v) and filter(Boolean) still infer a plain boolean, not a predicate, because "if false, x is not T" fails for those โ which is exactly your falsy-vs-nullish point in type-checker terms. isNotNil is still worth having as a name for the repeated case, but for a one-off nullish filter on 5.5+ you can drop the explicit guard signature and TypeScript will narrow it for you.
Great point! ๐ฑ
Youโre right that since TypeScript 5.5, an inline nullish check can infer the type predicate automatically, so the explicit
value is Tannotation isnโt required.Iโm still keeping the explicit predicate in the article examples mainly for readability, because it makes the intended narrowed type obvious at the call site.
So Iโll clarify that itโs optional on 5.5+, rather than implying itโs necessary.
And yes, the truthiness distinction is exactly the interesting part here,
filter(Boolean)/!!valuestill donโt give the same guarantee.Thanks for pointing it out! ๐ธ