# TypeScript Patterns for Environment Variables

> Source: <https://dev.to/chocoscoding/typescript-patterns-for-environment-variables-5eeg>
> Published: 2026-06-16 00:22:00+00:00

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

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

I was wondering... why use `.filter(Boolean)`

here? 🤔 The fallbacks already guarantee strings.

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

``` js
const allowedOrigins: string[]
```

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

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

My type definition changed to:

``` js
const allowedOrigins: (string | undefined)[]
```

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

in there?

`.filter(Boolean)`

Even Do?
`Boolean`

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

```
false
null
undefined
0
""
NaN
```

So:

```
["https://app.com", "", undefined].filter(Boolean)
// Result: ["https://app.com"]
```

At runtime, this works exactly as you'd expect. No `undefined`

survives. So why does TypeScript disagree? 🤷♀️

TypeScript is a transpiler. It doesn't execute `.filter(Boolean)`

— it only looks at types.

When it sees this:

```
array.filter(Boolean)
```

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.

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

``` js
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'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.

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;
}
```

Then:

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

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

So why did the AI-generated version — with the `||`

fallbacks — give `string[]`

without needing a predicate?

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

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.

`.filter(Boolean)`

`(string | undefined)[]`

`.filter((x): x is string => Boolean(x))`

`string[]`

`.filter(isDefined)`

`string[]`

`process.env.X || "fallback"`

`string`

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 👍
