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
| Pattern | Type | Use 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" | string | Guaranteed 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
- Audit your codebase for
.filter(Boolean)calls where the array containsundefinedornull. - Replace them with
.filter(isDefined)if you need correct types. - Add
isDefinedto 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.


