{"slug": "typescript-patterns-for-environment-variables", "title": "TypeScript Patterns for Environment Variables", "summary": "A developer discovered that TypeScript's type inference does not narrow types after using .filter(Boolean) on an array containing undefined values, leaving the type as (string | undefined)[] despite runtime correctness. The developer explains that using a type predicate like .filter((origin): origin is string => Boolean(origin)) or a utility function isDefined correctly narrows the type to string[]. The post highlights the gap between runtime behavior and TypeScript's static analysis.", "body_md": "Yesterday, as I was working on a CORS configuration, AI generated a block of code for me:\n\n``` js\nconst allowedOrigins = [\n  process.env.FRONTEND_URL || \"http://localhost:3000\",\n  process.env.ADMIN_URL || \"http://localhost:3001\",\n].filter(Boolean);\n```\n\nI was wondering... why use `.filter(Boolean)`\n\nhere? 🤔 The fallbacks already guarantee strings.\n\nSo I hovered on the variable. The type definition read:\n\n``` js\nconst allowedOrigins: string[]\n```\n\nFine. Made sense. But then I got curious. What if I removed the hardcoded fallbacks?\n\n``` js\nconst allowedOrigins = [\n  process.env.FRONTEND_URL,\n  process.env.ADMIN_URL,\n].filter(Boolean);\n```\n\nMy type definition changed to:\n\n``` js\nconst allowedOrigins: (string | undefined)[]\n```\n\n*I was shocked.* I just filtered the array. How can TypeScript still think there's an `undefined`\n\nin there?\n\n`.filter(Boolean)`\n\nEven Do?\n`Boolean`\n\nused as a filter function removes any falsy value from an array:\n\n```\nfalse\nnull\nundefined\n0\n\"\"\nNaN\n```\n\nSo:\n\n```\n[\"https://app.com\", \"\", undefined].filter(Boolean)\n// Result: [\"https://app.com\"]\n```\n\nAt runtime, this works exactly as you'd expect. No `undefined`\n\nsurvives. So why does TypeScript disagree? 🤷♀️\n\nTypeScript is a transpiler. It doesn't execute `.filter(Boolean)`\n\n— it only looks at types.\n\nWhen it sees this:\n\n```\narray.filter(Boolean)\n```\n\nIt knows the callback returns a `boolean`\n\n. But it doesn't know what that *means* for the type of the elements that survive. It can't infer \"if `Boolean(x)`\n\nis true, then `x`\n\nmust be a string.\" So the `undefined`\n\nstays in the type — even though it'll never actually be there at runtime.\n\nThat's the gap: your runtime behavior is correct, but your types are lying.\n\nTypeScript lets you close that gap with a **type predicate** — a way of explicitly telling the compiler what a filter function guarantees:\n\n``` js\nconst allowedOrigins = [\n  process.env.FRONTEND_URL,\n  process.env.ADMIN_URL,\n].filter((origin): origin is string => Boolean(origin));\n// Type: string[] ✅\n```\n\nThe `origin is string`\n\npart 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.\n\nIf you're doing this pattern often across a codebase, pull it into a small utility:\n\n```\nfunction isDefined<T>(value: T | undefined | null): value is T {\n  return value != null;\n}\n```\n\nThen:\n\n``` js\nconst allowedOrigins = [\n  process.env.FRONTEND_URL,\n  process.env.ADMIN_URL,\n].filter(isDefined);\n// Type: string[] ✅\n```\n\nReusable, self-documenting, and sexy 😍. I personally prefer this.\n\nSo why did the AI-generated version — with the `||`\n\nfallbacks — give `string[]`\n\nwithout needing a predicate?\n\n``` js\nconst allowedOrigins = [\n  process.env.FRONTEND_URL || \"http://localhost:3000\",\n  process.env.ADMIN_URL || \"http://localhost:3001\",\n].filter(Boolean);\n```\n\nBecause `process.env.X || \"fallback\"`\n\nalways evaluates to a `string`\n\n. The fallback string covers the `undefined`\n\ncase, so TypeScript already knows every element is a `string`\n\nbefore the filter runs. The `.filter(Boolean)`\n\nthere is just a defensive move — useful if someone later adds an entry without a fallback, but not needed for type correctness.\n\n`.filter(Boolean)`\n\n`(string | undefined)[]`\n\n`.filter((x): x is string => Boolean(x))`\n\n`string[]`\n\n`.filter(isDefined)`\n\n`string[]`\n\n`process.env.X || \"fallback\"`\n\n`string`\n\nThe lesson: `filter(Boolean)`\n\nis 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.\n\nThanks for reading 👍", "url": "https://wpnews.pro/news/typescript-patterns-for-environment-variables", "canonical_source": "https://dev.to/chocoscoding/typescript-patterns-for-environment-variables-5eeg", "published_at": "2026-06-16 00:22:00+00:00", "updated_at": "2026-06-16 00:47:28.274540+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["TypeScript"], "alternates": {"html": "https://wpnews.pro/news/typescript-patterns-for-environment-variables", "markdown": "https://wpnews.pro/news/typescript-patterns-for-environment-variables.md", "text": "https://wpnews.pro/news/typescript-patterns-for-environment-variables.txt", "jsonld": "https://wpnews.pro/news/typescript-patterns-for-environment-variables.jsonld"}}