DEV Community

Cover image for TypeScript Patterns for Environment Variables

TypeScript Patterns for Environment Variables

Yesterday, as I was working on a CORS configuration, AI generated a block of code for me:

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

I was wondering... why use .filter(Boolean) here? 🤔 The fallbacks already guarantee strings.

So I hovered on the variable. The type definition read:

const allowedOrigins: string[]
Enter fullscreen mode Exit fullscreen mode

Fine. Made sense. But then I got curious. What if I removed the hardcoded fallbacks?

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

My type definition changed to:

const allowedOrigins: (string | undefined)[]
Enter fullscreen mode Exit fullscreen mode

I was shocked. I just filtered the array. How can TypeScript still think there's an undefined in there?


First: What Does .filter(Boolean) Even Do?

Boolean used as a filter function removes any falsy value from an array:

false
null
undefined
0
""
NaN
Enter fullscreen mode Exit fullscreen mode

So:

["https://app.com", "", undefined].filter(Boolean)
// Result: ["https://app.com"]
Enter fullscreen mode Exit fullscreen mode

At runtime, this works exactly as you'd expect. No undefined survives. So why does TypeScript disagree? 🤷‍♀️


The Real Answer: TypeScript Doesn't Run Your Code

TypeScript is a transpiler. It doesn't execute .filter(Boolean) — it only looks at types.

When it sees this:

array.filter(Boolean)
Enter fullscreen mode Exit fullscreen mode

It knows the callback returns a boolean. But it doesn't know what that means for the type of the elements that survive. It can't infer "if Boolean(x) is true, then x must be a string." So the undefined stays in the type — even though it'll never actually be there at runtime.

That's the gap: your runtime behavior is correct, but your types are lying.


The Fix: Type Predicates

TypeScript lets you close that gap with a type predicate — a way of explicitly telling the compiler what a filter function guarantees:

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

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


The Reusable Helper

If you're doing this pattern often across a codebase, pull it into a small utility:

function isDefined<T>(value: T | undefined | null): value is T {
  return value != null;
}
Enter fullscreen mode Exit fullscreen mode

Then:

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

Reusable, self-documenting, and sexy 😍. I personally prefer this.


Back to the Original Code

So why did the AI-generated version — with the || fallbacks — give string[] without needing a predicate?

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

Because process.env.X || "fallback" always evaluates to a string. The fallback string covers the undefined case, so TypeScript already knows every element is a string before the filter runs. The .filter(Boolean) there is just a defensive move — useful if someone later adds an entry without a fallback, but not needed for type correctness.


Quick Reference

  • .filter(Boolean)

    • type def: (string | undefined)[]
    • Use when: You don't care about the resulting type.
  • .filter((x): x is string => Boolean(x))

    • Type def: string[]
    • Use when: Inline, one-off.
  • .filter(isDefined)

    • Type def: string[]
    • Use when: Reusable across a codebase.
  • process.env.X || "fallback"

    • Type def: string
    • Use when: You want a guaranteed default.

The lesson: filter(Boolean) is a runtime thing that TypeScript treats as a black box. When you need your types actually to reflect what's in the array, reach for a type predicate. Small change, honest types.

Thanks for reading 👍

Top comments (0)