The Problem: .filter(Boolean) Doesn't Narrow Types

Yesterday, while working on a CORS configuration, AI generated this code:

const allowedOrigins = [
  process.env.FRONTEND_URL || "http://localhost:3000",
  process.env.ADMIN_URL || "http://localhost:3001",
].filter(Boolean);

The type of allowedOrigins is string[]. That's correct because the fallbacks guarantee each element is a string before the filter runs.

But remove the fallbacks:

const allowedOrigins = [
  process.env.FRONTEND_URL,
  process.env.ADMIN_URL,
].filter(Boolean);

The type becomes (string | undefined)[]. Wait — filter(Boolean) removes all falsy values, including undefined. So at runtime, allowedOrigins will only contain strings. But TypeScript still thinks undefined might be there.

Why TypeScript Gets It Wrong

TypeScript is a transpiler — it doesn't execute .filter(Boolean). It only looks at the types. Boolean as a callback returns boolean, but TypeScript can't infer that Boolean(x) being true implies x is a string. The callback's return type is boolean, not a type predicate. So TypeScript keeps the original element type, including undefined.

This is a gap between runtime behavior and type safety. Your code works, but your types lie.

The Fix: Type Predicates

Type predicates tell TypeScript exactly what a filter function guarantees:

const allowedOrigins = [
  process.env.FRONTEND_URL,
  process.env.ADMIN_URL,
].filter((origin): origin is string => Boolean(origin));
// Type: string[] ✅

The origin is string part is the predicate. It promises the compiler: "if this function returns true, the value is definitely a string." TypeScript trusts that and narrows the type.

Reusable Helper: isDefined

For repeated use across a codebase, create a utility:

function isDefined(value: T | undefined | null): value is T {
  return value != null;
}

Then:

const allowedOrigins = [
  process.env.FRONTEND_URL,
  process.env.ADMIN_URL,
].filter(isDefined);
// Type: string[] ✅

This is reusable, self-documenting, and works for any type.

Back to the Original Code

Why did the AI version with || fallbacks give string[]? Because process.env.X || "fallback" always evaluates to a string. TypeScript knows every element is a string before the filter runs. The .filter(Boolean) there is just defensive — useful if someone later adds an entry without a fallback, but not needed for type correctness.

Quick Reference

PatternTypeUse Case
.filter(Boolean)(string | undefined)[]Don't care about resulting type
.filter((x): x is string => Boolean(x))string[]Inline, one-off
.filter(isDefined)string[]Reusable across codebase
process.env.X || "fallback"stringGuaranteed default

The Lesson

filter(Boolean) is a runtime pattern that TypeScript treats as a black box. When you need accurate types, use a type predicate. Small change, honest types.

Next Steps

  1. Audit your codebase for .filter(Boolean) calls where the array contains undefined or null.
  2. Replace them with .filter(isDefined) if you need correct types.
  3. Add isDefined to your project's utility library.

TypeScript's type system is powerful, but it has limits. Understanding type predicates closes the gap between runtime and compile-time guarantees.