Question

Using process.env in TypeScript: Read Node Environment Variables Safely

typescriptnode.js

Question

How can I read Node.js environment variables in TypeScript?

For example, accessing process.env.NODE_ENV produces this error:

Property 'NODE_ENV' does not exist on type 'ProcessEnv'.

I installed @types/node, but the error remains. How should Node environment variables be configured and typed in a TypeScript project?

Short Answer

You will learn how TypeScript represents process.env, why environment-variable values can be missing, how to ensure Node.js type definitions are loaded, and how to declare project-specific variables such as NODE_ENV safely.

Concept

Environment variables are values supplied to a program by its runtime environment rather than written directly into source code. They are commonly used for configuration such as ports, API URLs, feature flags, and deployment mode.

In Node.js, environment variables are exposed through process.env:

const mode = process.env.NODE_ENV;

A key detail is that an environment variable may not be set. Therefore, its value should be treated as string | undefined, not always as a string.

const port = process.env.PORT;
// Type: string | undefined

Current Node.js type definitions generally allow access to arbitrary keys on process.env. If TypeScript says a property does not exist, likely causes include:

  • Node type definitions are not included in the TypeScript project.
  • An old or conflicting Node type package is installed.
  • A local ProcessEnv declaration overrides or narrows the expected type.
  • A compiler setting requires bracket notation for index-signature properties.

For known application variables, declare their names and expected values with declaration merging. This provides autocomplete and catches misspellings while preserving the fact that optional variables may be absent.

Mental Model

Think of process.env as a shared wall of labeled lockers outside your application.

  • process.env is the wall of lockers.
  • NODE_ENV is one locker label.
  • The value inside is always text, because environment variables are passed to Node as strings.
  • Some lockers may be empty or may not exist at all, so TypeScript represents many values as undefined.

Adding a ProcessEnv declaration is like making an approved list of locker labels your application expects. TypeScript can then suggest valid labels and warn when you type the wrong one.

Take Quiz

Syntax and Examples

Read an environment variable from process.env:

const nodeEnv = process.env.NODE_ENV;

if (nodeEnv === "production") {
  console.log("Production mode");
}

Because nodeEnv may be missing, provide a fallback when appropriate:

const port = process.env.PORT ?? "3000";
console.log(`Server will use port ${port}`);

?? uses the value on the right only when the left side is null or undefined.

For project-specific type information, create a file such as src/environment.d.ts:

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV?: "development" | "test" | "production";
      PORT?: string;
      DATABASE_URL?: string;
    }
  }
}

export {};

Now TypeScript knows the accepted NODE_ENV values:

const mode = process.env.NODE_ENV;
// "development" | "test" | "production" | undefined

If noPropertyAccessFromIndexSignature is enabled and a variable has not been explicitly declared, use bracket notation:

const customValue = process.env["CUSTOM_SETTING"];

Bracket notation is useful for dynamically chosen variable names too:

const variableName = "PORT";
const value = process.env[variableName];

Step by Step Execution

Consider this startup configuration:

const rawPort = process.env.PORT;
const port = rawPort ?? "3000";
const isProduction = process.env.NODE_ENV === "production";

console.log({ port, isProduction });

Suppose the process starts with:

PORT=8080 NODE_ENV=production

Execution proceeds as follows:

  1. process.env.PORT reads the text value "8080", so rawPort is "8080".
  2. rawPort ?? "3000" keeps "8080", because it is not undefined.
  3. process.env.NODE_ENV === "production" compares two strings and evaluates to true.
  4. The output is equivalent to:
{ port: "8080", isProduction: true }

If PORT is not set, rawPort is undefined, and port becomes "3000". Notice that port remains a string. Convert it deliberately if your application needs a number:

const port = Number(process.env.PORT ?? "3000");

Real World Use Cases

Environment variables keep configuration separate from application code. Common uses include:

  • Server configuration: PORT, HOST, and logging level.
  • Database connections: DATABASE_URL or database host credentials.
  • Third-party services: API keys such as STRIPE_SECRET_KEY.
  • Deployment mode: NODE_ENV=development, test, or production.
  • Feature flags: ENABLE_BETA_SEARCH=true.
  • CI pipelines: providing temporary tokens and build configuration without committing secrets.

For example, an API server may select its behavior based on its environment:

const isProduction = process.env.NODE_ENV === "production";

if (isProduction) {
  console.log("Use production logging settings");
} else {
  console.log("Use development logging settings");
}
Take Quiz

Real Codebase Usage

Real applications usually read and validate environment variables once during startup, rather than scattering process.env access throughout the codebase.

A simple configuration module centralizes defaults and required-value checks:

function requireEnv(name: string): string {
  const value = process.env[name];

  if (!value) {
    throw new Error(`Missing required environment variable: ${name}`);
  }

  return value;
}

export const config = {
  nodeEnv: process.env.NODE_ENV ?? "development",
  port: Number(process.env.PORT ?? "3000"),
  databaseUrl: requireEnv("DATABASE_URL")
};

Other files then import config instead of reading process.env directly:

import { config } from "./config";

console.log(config.port);

This pattern gives several benefits:

  • Missing required configuration fails early at startup.
  • Parsing happens in one place.
  • Application code receives predictable values.
  • Tests can replace or construct configuration more easily.

If @types/node is installed but process is still unknown or incorrectly typed, verify that the project uses Node types. A typical tsconfig.json setup is:

{
  "compilerOptions": {
    "types": ["node"]
  }
}

Only add "types": ["node"] when the project has a types list. If types is omitted, TypeScript normally includes visible @types packages automatically. Restart the TypeScript language server or editor after changing dependencies or declaration files.

Common Mistakes

Assuming every variable exists

This is unsafe because PORT may be absent:

// Broken: `process.env.PORT` can be undefined.
const port = process.env.PORT.toUpperCase();

Use a fallback or validate it first:

const port = (process.env.PORT ?? "3000").toUpperCase();

Treating values as booleans or numbers automatically

Environment variables are strings.

// Incorrect: "false" is a non-empty string, so this is truthy.
const enabled = Boolean(process.env.ENABLED);

Parse the intended format explicitly:

const enabled = process.env.ENABLED === "true";
const timeout = Number(process.env.TIMEOUT_MS ?? "5000");

Using a non-null assertion without validation

// Compiles, but can fail at runtime if DATABASE_URL is missing.
const databaseUrl = process.env.DATABASE_URL!;

Prefer an explicit startup check:

if (!process.env.DATABASE_URL) {
  throw new Error("DATABASE_URL is required");
}

Replacing Node's declaration instead of extending it

Do not create a standalone ProcessEnv interface in the wrong scope. Extend the existing NodeJS.ProcessEnv interface instead:

declare global {
  namespace NodeJS {
    interface ProcessEnv {
      APP_NAME?: string;
    }
  }
}

export {};

Expecting a .env file to load itself

Node.js reads its environment from the running process. A .env file is only a file until a tool or runtime loads it. Configure your runtime or a suitable loader according to your project setup; do not assume TypeScript itself loads .env files.

Comparisons

ApproachBest forResulting typeMain trade-off
process.env.PORTSimple known variable accessUsually string | undefinedLess autocomplete for custom names unless declared
process.env["PORT"]Dynamic keys or index-signature settingsUsually string | undefinedMore verbose
process.env.PORT ?? "3000"Optional variables with a safe defaultstringA default can hide missing configuration when a value should be required
requireEnv("DATABASE_URL")Required variablesstring after validationMust handle startup error
Number(process.env.PORT ?? "3000")Numeric configurationnumberInvalid text can become NaN; validate when necessary
ProcessEnv declaration mergingDocumented, known application variablesSpecific union types plus possible undefinedTypes describe expectations but do not create runtime values
Take Quiz

Cheat Sheet

// Read a value: possibly undefined
const value = process.env.MY_VARIABLE;

// Use a default
const port = process.env.PORT ?? "3000";

// Check a specific value
const isProduction = process.env.NODE_ENV === "production";

// Read a dynamic key
const valueByName = process.env[variableName];

// Require a value
function requireEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(`Missing ${name}`);
  return value;
}
// environment.d.ts: add known variables
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      NODE_ENV?: "development" | "test" | "production";
      DATABASE_URL?: string;
    }
  }
}
export {};

Rules:

  • Environment-variable values are strings or undefined.
  • Use ?? for defaults, not || when an empty string is meaningful.
  • Parse numbers and booleans explicitly.
  • Validate required values at application startup.
  • Ensure @types/node is installed and included by the active tsconfig.json.

FAQ

Why is process.env.NODE_ENV possibly undefined in TypeScript?

The operating system or deployment environment may not define NODE_ENV. TypeScript reflects that possibility with undefined.

Does installing @types/node create environment variables?

No. It only supplies TypeScript declarations for Node.js APIs. Your shell, deployment platform, test runner, or configuration loader supplies actual values.

Why does TypeScript say NODE_ENV does not exist on ProcessEnv?

Check that the active TypeScript configuration includes Node types, update conflicting or old type packages, and inspect local .d.ts files that declare ProcessEnv. You can also explicitly extend NodeJS.ProcessEnv for your project.

Should NODE_ENV be required?

That depends on the application. Many apps default it to development; production deployments often validate that it is explicitly set.

How do I type a required environment variable?

A declaration can document its name, but runtime input can still be missing. Validate it with a helper such as requireEnv() before using it as a string.

Are environment variable values numbers or booleans?

No. Node receives them as strings. Convert values with Number(...) or compare them with a string such as value === "true".

Should I commit a .env file with secrets?

Do not commit real secrets. Commit a safe example file, such as .env.example, that lists required variable names without sensitive values.

Take Quiz

Mini Project

Description

Build a small TypeScript configuration module for a Node.js service. It reads environment variables once, uses safe defaults for optional settings, and stops startup when required database configuration is missing.

Goal

Create a typed config object that exposes a validated database URL, a numeric port, and a restricted application environment.

Requirements

Create an environment.d.ts file that declares NODE_ENV, PORT, and DATABASE_URL.

Take Quiz

Keep learning

Related questions

View more