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:
/**
* @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);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):
/** @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'.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:
/** @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'.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 = 5is TS2322: Node.js would store the text"5", and the types say so. WriteString(5). process.env.X ?? "default"is astring. A default with??narrows the type the same way a check does.- Browsers have no
process. In front-end code, bundlers commonly replaceprocess.env.Xorimport.meta.env.Xat 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
- process.env · Node.js documentation
- Declaration Merging (global augmentation) · TypeScript handbook
Last reviewed 21 October 2026.