DEV Community

Cover image for Inferred Type Predicates Won't Save filter(Boolean). Here's What Will.
Parsa Jiravand
Parsa Jiravand

Posted on Originally published at bestpractic.org

Inferred Type Predicates Won't Save filter(Boolean). Here's What Will.

Open any "what's new in TypeScript 5.5" roundup from the last two years and you'll find some version of the same line: inferred type predicates finally fixed .filter(Boolean). It gets repeated because it's satisfying — a years-old annoyance, solved for free, no code changes needed. It also isn't true.

The feature that's actually in 5.5 is narrower than the myth, and more useful once you see what it's really for. You've probably been deleting type guards you didn't need to delete, right next to a .filter(Boolean) that's just as untyped as it was in 2019.

The claim, and why it's believable

Here's the example every one of those roundups leads with. Before TypeScript 5.5, this didn't narrow:

const maybeNumbers = [1, null, 2, null, 3];

// previously: (number | null)[] — TypeScript didn't trust the callback
const numbers = maybeNumbers.filter((n) => n !== null);
Enter fullscreen mode Exit fullscreen mode

You wrote a callback that obviously removes null, ran it, and TypeScript still handed you back (number | null)[]. The only way to get a real number[] was to spell it out by hand:

const numbers = maybeNumbers.filter(
  (n): n is number => n !== null
);
Enter fullscreen mode Exit fullscreen mode

That (n): n is number => syntax is a type predicate — a little promise to the compiler that says "if this returns true, treat the input as this narrower type." Writing it by hand, every time, for a check this obvious, was the exact kind of busywork TypeScript is supposed to save you from. TypeScript 5.5 fixed it: the plain (n) => n !== null now gets the predicate inferred automatically. No annotation. Same code, better type.

So far, every word of the popular claim checks out. Here's where it quietly goes off the rails.

Try the one line everyone actually writes

Nobody filters nulls with n !== null in real code nearly as often as they reach for the shorthand:

const configs = [getConfig(), null, getConfig(), undefined];

// still (Config | null | undefined)[] in every TypeScript version as of this writing
const valid = configs.filter(Boolean);
Enter fullscreen mode Exit fullscreen mode

Paste that into the TypeScript playground. The result is still (Config | null | undefined)[] — the exact array you started with, falsy values and all. The feature that rewrote n !== null into a type guard does nothing here, on the single most common one-liner for exactly this job.

Quick gut check before you scroll: what's different between (n) => n !== null and Boolean, if they do the same job?

Why the compiler can see one and not the other

Inferred type predicates work by TypeScript reading a function's body and recognizing a pattern: one return statement, no implicit fall-through, the parameter isn't reassigned, and the thing being returned is a boolean check tied directly to that parameter. (n) => n !== null qualifies on every count — it's a tiny, transparent function, and the compiler can see exactly what it decides and why.

Boolean isn't a function you wrote. It's the global constructor, declared in TypeScript's own standard library as a call signature with no visible implementation — (value?: any) => boolean, full stop. There's no body for the inference engine to read, because as far as the type checker is concerned, there's no body at all. It's an ambient declaration, same category as parseInt or Array.isArray: TypeScript trusts the signature you gave it and nothing more. Teaching the compiler to special-case .filter(Boolean) specifically would mean hard-coding one function name into the type checker — not inference, a workaround with a name attached.

That's the actual boundary of the 5.5 feature: it infers predicates from code it can read. Boolean was never going to be included, in 5.5 or any release after it, unless the standard library itself grows a dedicated overload for it — which, as of this writing, it hasn't.

🎮 Try it yourself

▶️ Open the interactive playground →

Runs right in your browser — poke at it and watch the concept react live.

Why the mix-up happened

Nobody misreported this out of carelessness. .filter(n => n !== null) and .filter(Boolean) solve the identical problem and read, to a human, like two spellings of the same idea. The release notes' headline example uses the first form because it's the clean one to explain in a changelog. Readers who've spent years typing the second form saw "filter now narrows" and reasonably assumed their own habit was covered.

It's the kind of gap that only shows up when you actually run the code, which most roundups — understandably, on a deadline — don't.

What to actually do with your type guards

The real payoff of 5.5 is that you can delete annotations you were writing by hand, as long as the function itself is one TypeScript can read:

// before 5.5 — had to spell out the predicate yourself
function isDefined<T>(value: T): value is NonNullable<T> {
  return value != null;
}

// 5.5+ — this now infers the exact same predicate, with no `is` anywhere
function isDefined<T>(value: T) {
  return value != null;
}

const valid = configs.filter(isDefined); // Config[] — for real this time
Enter fullscreen mode Exit fullscreen mode

That's a genuine win: a helper function you've probably had in a utils.ts file for years just got shorter, and any arrow function shaped like it gets the same treatment for free. What it isn't is a reason to rip out isDefined and go back to .filter(Boolean) for brevity — that one line is exactly where the narrowing still doesn't reach. If you want the one-liner and the types, write the predicate yourself:

const valid = configs.filter(
  (c): c is NonNullable<typeof c> => Boolean(c)
);
Enter fullscreen mode Exit fullscreen mode

Same runtime behavior as .filter(Boolean), three extra words, and an array type you can actually trust downstream.

The takeaway

TypeScript 5.5 didn't make filter(Boolean) type-safe — it made your own boolean-returning functions smart enough that you stop writing is annotations for them by hand. Keep the named helper, drop its predicate syntax, and reach for the explicit inline predicate only when you actually want the Boolean-style one-liner. The compiler was never going to read minds; it just got better at reading your functions.

Next time a changelog claims a one-line fix for a years-old annoyance, paste the actual snippet into the playground before you repeat it. What's one you've seen repeated that didn't hold up?

🧠 Test yourself

Think it clicked? Take the 7-question quiz →

Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.


🚀 Want more like this? Every guide, playground, and quiz lives on bestpractic.org — open it and sign up free so the next one finds you.

Thanks for reading! Let's stay connected:

Top comments (0)