When working with Node.js its a common practice to keep our credentials and secrets in a separate .env
file, that's never pushed to our repo. In order to access these variables in our code, we simply use dotenv package to parse the .env
file and load our env variables into process.env
.
Example
Here's a quick example on how to do this in plain JavaScript.
// .env
API_KEY = <YOUR_API_KEY>
DB_URI = <YOUR_DB_URL>
....
Now to access these variables we can do something like
// index.js
const dotenv = require('dotenv');
dotenv.config()
const connectDB = () => {
...
mongooose.connect(process.env.DB_URI)
...
}
Since JavaScript doesn't care much about the type so we can pretty much access any property on process.env
and our code won't give us any red signs untill we run it and find out that process.env.DB_URI is undefined
.
Now let's say we had to do the same thing in TypeScript, and keep it fully typed and have all the benefits that come with a typed system. In order to keep it fully typed we will have to first extend the ProcessEnv
Interface to have our custom env variables available on the interface. For this we will have to use Declaration-Merging which is a TypeScript feature for combining two or more declarations with the same name.
You can have a quick overview of this feature and its use case on this twitter thread
We will use the same feature to extend the ProcessEnv
interface with our custom env varibales.
Create a file named global.d.ts
with the below contents
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: string;
PORT: string;
MONGO_URI: string;
}
}
Above we are just extending the ProcessEnv Interface that's accessible under NodeJS namespace and specifying our custom env varibales types.
Now if you write process.env
in your code, intellisense would autosuggest you the fields avaialable on process.env
which is great process.env.MONGO_URI
is no longer a mystery in our code.
Make sure you specify the path to this file in your tsconfig.json
A better approach would be to have a folder lets say extended-types
and keep all your extended types there and just specify the path to that folder here instead of a single file.
"typeRoots": ["./global.d.ts", "./node_modules/@types"]
Now let's create a config.ts
file that will encapsulate the logic for parsing .env
file and converting it into our own custom Config
type that we want to use in the code.
import path from "path";
import dotenv from "dotenv";
// Parsing the env file.
dotenv.config({ path: path.resolve(__dirname, "../config/config.env") });
// Interface to load env variables
// Note these variables can possibly be undefined
// as someone could skip these varibales or not setup a .env file at all
interface ENV {
NODE_ENV: string | undefined;
PORT: number | undefined;
MONGO_URI: string | undefined;
}
interface Config {
NODE_ENV: string;
PORT: number;
MONGO_URI: string;
}
// Loading process.env as ENV interface
const getConfig = (): ENV => {
return {
NODE_ENV: process.env.NODE_ENV,
PORT: process.env.PORT ? Number(process.env.PORT) : undefined,
MONGO_URI: process.env.MONGO_URI
};
};
// Throwing an Error if any field was undefined we don't
// want our app to run if it can't connect to DB and ensure
// that these fields are accessible. If all is good return
// it as Config which just removes the undefined from our type
// definition.
const getSanitzedConfig = (config: ENV): Config => {
for (const [key, value] of Object.entries(config)) {
if (value === undefined) {
throw new Error(`Missing key ${key} in config.env`);
}
}
return config as Config;
};
const config = getConfig();
const sanitizedConfig = getSanitzedConfig(config);
export default sanitizedConfig;
Now you can simple import config.ts in your code and access the fields and it would be super cool to have the intelllisense give you type inference about the fields present on config object.
import mongoose from "mongoose";
import config from "./config";
const connectDB = async () => {
const connection = await mongoose.connect(config.MONGO_URI);
console.log(`🟢 Mongo db connected:`, connection.connection.host);
};
I hope this post helped you and you can extend the same concept to other similar use cases when working with TypeScript.
Feel free to drop any suggestions or improvements on my approach for this, and you can always connect with me on twitter
Top comments (10)
I think you should export
sanitizedConfig
instead ofexport default verifiedConfig
.Also, rather than maintaining those two, almost identical interfaces, one could use
export type Config = Required<ENV>
. Otherwise, a fantastic post! This solution should be part of a dotenv-ts package.Thanks for spotting that out its a typo
Finally!!! Thank you so much!!
I know this is kind of an old post, but this saved me while working with .env in Typescript. Nothing else I found really gave me a proper solution, so thank you!
Great article.
Exactly what I needed. Thank you!
Good work man!
useful. Thank you
This is so helpful, thanks for making my life easier with .env variables. Such pain before having to manually check each var before use.
Thank you, thanks for making my life easier with .env variables