DEV Community

Asjad Anis
Asjad Anis

Posted on • Updated on

Parsing .env with TypeScript

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>
....
Enter fullscreen mode Exit fullscreen mode

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)
  ...
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

image

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;


Enter fullscreen mode Exit fullscreen mode

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.

image

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);
};

Enter fullscreen mode Exit fullscreen mode

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 (9)

Collapse
 
adrian19hub profile image
adrian19hub

Finally!!! Thank you so much!!

Collapse
 
bykiandra profile image
Kiandra Ginevra

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!

Collapse
 
codeofarmz profile image
codeOfArmz

I think you should export sanitizedConfig instead of export 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.

Collapse
 
asjadanis profile image
Asjad Anis

Thanks for spotting that out its a typo

Collapse
 
dolimovkhumoyun profile image
dolimovkhumoyun

Great article.

Collapse
 
andonary profile image
Ando NARY

useful. Thank you

Collapse
 
netk profile image
David Quintero 📿

Exactly what I needed. Thank you!

Collapse
 
warengonzaga profile image
Waren Gonzaga

Good work man!

Collapse
 
axanthus profile image
Aristos

This is so helpful, thanks for making my life easier with .env variables. Such pain before having to manually check each var before use.