Skip to content
R.

Why doesn't `filter(Boolean)` narrow nullable arrays in TypeScript?

`filter(Boolean)` removes falsy values at runtime, but TypeScript still needs a callback shape it can treat as a narrowing predicate. This article shows the difference, proves it with a small compiler fixture, and explains when a truthiness filter is the wrong tool because `0` or `""` would disappear too.

About 6 min readComments

You reach for `filter(Boolean)` because you want a quick cleanup step: remove `undefined`, keep the real values, and move on. Then the next line still refuses to accept the filtered array as `string[]`, or worse, the code compiles only after a cast that hides the real problem.

The mismatch is not in the runtime behavior. The array is being filtered. The mismatch is in the type contract: a truthy callback is not the same thing as a narrowing predicate, and TypeScript only narrows when the callback form gives the checker enough information to prove which union members remain.

That distinction matters because the next line often wants to trust the filtered result without another cast. If the callback does not expose the rule that removed `undefined`, the compiler has no reason to treat the array as fully non-nullable just because the runtime output happened to look clean.

Reproduce the result that feels wrong

A nullable array makes the problem easy to see. The runtime filter does remove falsy entries, but the type on the resulting array is still not the narrowed type you wanted. That is the point where many codebases add a cast and move on, which hides the contract mismatch instead of fixing it.

The failure is not that the callback does nothing. It does real work at runtime. The failure is that `Boolean` is too generic to tell the checker which members of the union survived, so the resulting array type stays wider than the runtime output.

That distinction matters because the type system is trying to preserve a proof, not just a value. If the callback does not expose the rule that removed `undefined`, the checker cannot safely replace `Array<string | undefined>` with `string[]` just because the runtime output happened to be truthy. A cast can silence the complaint, but it also removes the proof the next line was supposed to rely on.

The useful mental model is to think in terms of ownership: the filter callback owns the claim about which values survive, and the next statement owns the right to rely on that claim. When the callback is vague, the proof is vague too. When the callback says exactly what it removes, the rest of the function can stop second-guessing the array shape.

Runtime intent versus type intent
Callback formRuntime behaviorTypeScript resultSafe when
`filter(Boolean)`Drops every falsy valueDoes not express a narrowing predicate by itselfYou truly want to remove `0`, `false`, `""`, `null`, and `undefined` together
`filter((value): value is string => value !== undefined)`Drops only `undefined` in this exampleNarrows the output to `string[]`You only want the nullable member removed

SourcesTypeScript handbook: narrowing (opens a new tab)

Why a truthy callback is not the same as a predicate

TypeScript distinguishes between a boolean-valued callback and a callback that communicates a specific refinement. The handbook treats truthiness checks as one form of narrowing in control flow, but a generic callback such as `Boolean` does not tell the compiler which union members remain after filtering.

The practical consequence is that the type checker needs either an explicit predicate signature or a boolean expression it can infer as a predicate. The release notes for TypeScript 5.5 describe inferred type predicates for certain callback shapes, but that is still not the same thing as assuming every truthy filter is a narrowing guard.

A useful mental model is that the compiler can trust a predicate when the callback itself says what is being excluded. `value !== undefined` says the nullable member is gone. `value != null` says both nullish members are gone. `Boolean` says only that the result should be truthy at runtime, which is a broader condition and therefore not a precise narrowing proof.

This is why the article needs both the handbook and the 5.5 notes. The handbook explains that truthiness can participate in narrowing when the checker has a visible condition. The release notes explain that some callbacks can now infer predicates from their bodies. Neither source says that a generic helper with a wide truthiness contract automatically becomes a type guard, and that omission is exactly the edge the reader is running into.

Broken example / ts
const values: Array<string | undefined> = ['a', undefined, 'b'];
const filtered = values.filter(Boolean);

// `filtered` still behaves like a wider array type here.
const stillAllowed: (typeof filtered)[number] = undefined;

Repaired example / ts
const values: Array<string | undefined> = ['a', undefined, 'b'];
const filtered = values.filter((value): value is string => value !== undefined);

// @ts-expect-error `undefined` is no longer part of the result type.
const notAllowed: (typeof filtered)[number] = undefined;

SourcesTypeScript 5.5 release notes: inferred type predicates (opens a new tab)

Choose the intent before choosing the filter

The deeper reason this matters is that `Boolean` encodes a different business rule. It removes all falsy values, not just nullish ones. That is often a bug when `0` is a legitimate count, `false` is a valid flag, or `""` is a meaningful empty label.

If the goal is to keep only non-nullish values, the code should say that directly. If the goal is to remove all falsy values, then the wider effect is intentional and the wider type or runtime loss is part of the decision. The article is narrow on purpose: it is about choosing the right callback for the intended contract, not about inventing a universal cleanup idiom.

That narrowness is also why the local fixture matters. It proves a small, repeatable claim about one array and one callback shape. It does not try to prove the whole language. The point is to show that a reader's intuition about "truthy" and the compiler's notion of "narrowed" are related but not identical, and that the difference becomes important as soon as later code wants to trust the filtered result without another cast.

In practice, this means the repaired callback is not just a type-system trick. It is a better description of intent for the human reviewer too. A future edit to the same line can answer a simple question: did we still mean to keep zero and empty strings, or did the filter intentionally get broader? The article gives the reader a pattern for answering that question without re-deriving the language rule from scratch.

  • Use a truthiness filter only when `0`, `false`, and `""` are also supposed to disappear.
  • Use an explicit predicate when the array should keep every valid falsy payload except `null` or `undefined`.
  • Treat a cast as a signal that the callback and the intent do not yet match.
Share LinkedIn Email Subscribe

Discussion

Leave a comment

Comments appear after review. No email needed.

Image preview

Follow the blog

New articles in your feed. No email needed.

Use OpenRSS

Preview the feed, then choose a reader to subscribe.

Open in OpenRSS (opens a new tab)

Already have a reader?

Paste this link into your reader's Add feed option.

Get full articles in your reader, not your inbox. View XML feed