Recipe · Modules and packages

How do I type process.env variables in JavaScript?

  • Node.js (ES modules)
  • process.env
  • @types/node
  • Configuration
  • Checked with TypeScript 5.9

The problem tsc says process.env.DATABASE_URL might be undefined every time I pass it somewhere. The variable is always set in production. What is the right way to type environment variables in a Node.js project?

The short answer

Read every variable in one config module, check it there, and export plain typed values. The rest of the program imports the config and never touches process.env:

config.js config.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/**
 * @param {string} name
 * @returns {string}
 */
function required(name) {
  const value = process.env[name];
  if (value === undefined || value === "") throw new Error(`Missing environment variable ${name}`);
  return value;
}

export const config = {
  databaseUrl: required("DATABASE_URL"),
  port: Number(process.env.PORT ?? 3000),
  debug: process.env.DEBUG === "1",
};

console.log(new URL(config.databaseUrl).host, config.port.toFixed(0), config.debug);
function required(name: string): string {
  const value = process.env[name];
  if (value === undefined || value === "") throw new Error(`Missing environment variable ${name}`);
  return value;
}

export const config = {
  databaseUrl: required("DATABASE_URL"),
  port: Number(process.env.PORT ?? 3000),
  debug: process.env.DEBUG === "1",
};

console.log(new URL(config.databaseUrl).host, config.port.toFixed(0), config.debug);
Read each variable once, check it, export typed values.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js"]
}

required() returns a string because it throws otherwise, so config.databaseUrl is a string everywhere; port is a number and debug a boolean.

Why it works

@types/node declares process.env as an object whose values are string | undefined, which is the truth: any variable may be missing. Checking once and throwing early turns that uncertainty into a startup error with a clear message, instead of a TypeError deep in the program. tsc follows the check: after value === undefined throws, value is a string.

The same thing in TypeScript

The TypeScript equivalent tab is the same module with the parameter and return type written inline. Nothing about process.env changes between the two.

What goes wrong

Passing a variable straight to a function that needs a string fails, because it may be undefined (TS2345):

config.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: config.js(7,9): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. config.js(7,9): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. config.js(7,9): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'. checked 21 October 2026
/** @param {string} url */
function connect(url) {
  return new URL(url);
}

connect(process.env.DATABASE_URL);error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
  Type 'undefined' is not assignable to type 'string'.
process.env values are string | undefined.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js"]
}

You can also declare the variables on NodeJS.ProcessEnv in a .d.ts file. That types DATABASE_URL as a string, but it is only a declaration: nothing checks that the variable is set, so a missing one surfaces as undefined at runtime. Optional ones stay string | undefined:

config.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: config.js(8,9): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. config.js(8,9): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. config.js(8,9): error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'. checked 21 October 2026
/** @param {string} url */
function connect(url) {
  return new URL(url);
}

connect(process.env.DATABASE_URL);
connect(process.env.PORT);error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
  Type 'undefined' is not assignable to type 'string'.
Declaring the variables in a .d.ts types them, without checking them.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js", "*.d.ts"]
}

Gotchas

  • Only strings go in. process.env.PORT = 5 is TS2322: Node.js would store the text "5", and the types say so. Write String(5).
  • process.env.X ?? "default" is a string. A default with ?? narrows the type the same way a check does.
  • Browsers have no process. In front-end code, bundlers commonly replace process.env.X or import.meta.env.X at build time; declare those names the way the globals recipe does.

Questions

Does node --env-file change the types?

No. Node.js reads the file at startup, but tsc never sees it; process.env keeps the type @types/node gives it. Check the values in code, as the config module does.

How do I type a numeric variable like PORT?

Convert it where you read it, Number(process.env.PORT ?? 3000), and check the result with Number.isInteger if a bad value should stop the program. The environment only ever holds strings.

Sources

Last reviewed 21 October 2026.