Type Strict Environment Variables
Tags: typescript, node
Published: 2022-10-22 15:15:25

---
title: Type Strict Environment Variables
published: true
date: 2022-10-22 15:11:09 UTC
tags: Typescript, NodeJS
canonical_url: https://www.codingmatty.com/type-strict-environment-variables/
---

I love Typescript.

And Environment variables are one of the essential parts of storing run-time configuration securely.

But one of the most significant issues with using environment variables within a Typescript application is the lack of types.

Let's say you are trying to use the `PORT` environment variable, but you end up getting an error like this:

![](https://www.codingmatty.com/content/images/2022/10/image.png)

Because the default type for **every** environment variable is `string | undefined`, you end up having to validate them in creative ways. Here is one solution to the above problem:

```
const port: number = process.env.PORT ? parseInt(process.env.PORT) : 4000;

startServer(port);
```

But how pretty does that look? 🤮

And this is just an example of one environment variable that needs to be validated. What about multiple variables? What about enumerations (i.e. `development`, `production`)? Using environment variables on their own is kind of a bane. So let's look at a better way.

I almost decided to build another library myself to provide a solution to this issue, but I decided to check out what's out there first.

Luckily, there are quite a few projects that have sought to solve this problem 😄

## Solution

First, what would be an ideal solution?

- Declarative-like. I initially thought a single yaml file to describe the environment would work, but I learned that it's not that easy to convert a yaml file to Typescript types without an extra step.
- Useful errors. What environment variable(s) are not set up correctly and why? Is a variable not a number? Not the correct enumeration? Whatever the solution is should describe the problem well.
- Ability to derive other configurations from environment variables. This one is more of a nice-to-have feature. I like to have booleans like `isEmailEnabled` to enable/disable emails from being sent when the API Key isn't provided instead of checking for the existence of the API Key directly.

I ended up deciding to use `env-var`.

![](https://www.codingmatty.com/content/images/2022/10/env-var-npm.png)
_env-var usage stats on NPM as of 10/22/22_

It uses a fully type-safe builder pattern API which allows you to set the conditions by chaining functions together, making it relatively readable.

As a small example, this is how we can define the `port` variable in the example above:

```
const port: number = env.get('PORT').default(4000).asPortNumber();

startServer(port);
```

This looks much nicer, albeit more verbose, but way more readable. The `asPortNumber()` is the function that triggers validation, but it also has the bonus of making sure the port number is valid, i.e. is it between 0 and 65535?

To encapsulate all environment variables into a single place and avoid piecemealed validations throughout the codebase, I created a `config.ts` file at the root of my source directory.

Below is a code snippet of my config file with most of my environment variables.

```
// config.ts
import env from 'env-var';

export const port = env.get('PORT').default(4000).asPortNumber();
export const environment = env
  .get('NODE_ENV')
  .default('development')
  .asEnum(['production', 'staging', 'test', 'development']);
export const isDeployed =
  environment === 'staging' || environment === 'production';

export const secret = env.get('SECRET').required().asString();
export const sentryDsn = env.get('SENTRY_DSN').asUrlString();

export const sendgridApiKey = env.get('SENDGRID_API_KEY').asString();
export const sendgridFromEmail = env
  .get('SENDGRID_FROM_EMAIL')
  .required(!!sendgridApiKey)
  .asString();
export const isEmailEnabled = sendgridApiKey && sendgridFromEmail;

export const redisHost = env.get('REDIS_HOST').asString();
export const redisPort = env.get('REDIS_PORT').asPortNumber();
export const redisConfig =
  redisHost && redisPort
    ? {
        host: redisHost,
        port: redisPort,
      }
    : undefined;
```

Usage would look like this:

```
import * as config from './config'
const port: number = config.port;

// OR

import { environment } from './config'
```

Now, I have limited the need to check for any environment variable to be `undefined`, and my types are strict. I am even able to derive some extra variables to make my code more readable, like how `isDeployed` is being derived from the value of `environment`.

Here is a quick explanation of some of the variables:

- `port: number` - defaulted to 4000.
- `environment: EnvrionmentEnum` - defaulted to `development` .
- `isDeployed: boolean` - derives from the `environment` variable.
- `secret: string` - will throw an error if not defined.
- `sentryDsn: string` - will throw an error if it's not in URL format.
- `sendgridApiKey: string | undefined` - optional API key for Sendgrid.
- `sendgridFromEmail: string | undefined` - will throw an error if it's not defined but the SendGrid API Key _is_ defined.
- `isEmailEnabled: boolean` - derives from whether other Sendgrid variables are defined. 
- `redisConfig` - an `IORedis` configuration object based on environment variables `REDIS_HOST` and `REDIS_PORT`. 

You can even do a quick check of a dotenv file (or check your system's configuration) via a command like:

```
# .env
PORT="xyz"

---

$ npx dotenv - .env -- ts-node config.ts

.../node_modules/env-var/lib/variable.js:47
    throw new EnvVarError(errMsg)
          ^
EnvVarError: env-var: "PORT" should be a valid integer
```

OR

```
$ NODE_ENV="test" npx ts-node src/config.ts

.../node_modules/env-var/lib/variable.js:47
    throw new EnvVarError(errMsg)
          ^
EnvVarError: env-var: "NODE_ENV" should be one of [production, staging, test, development]
```

This will help you confirm whether your .env file is set up correctly or tell you exactly what errors you have.

## Alternatives

Disclaimer: I haven't tested any of these out, but I figured I wanted to mention them for completion.

- `tconf`  
 This library seemed like what I wanted to use, but it requires a lot of different components, including a type file, a yaml configuration file, and a file to glue all of those together. It might be good for larger projects with many variables, but it seemed overkill for my use case. 
- `unified-env`  
 This library seems robust, allowing you to tie in variables from the environment as CLI arguments and from a .env file. It provides one file to parse all of the variables with validation options.  
- [`@velsa/ts-env`](https://www.npmjs.com/package/@velsa/ts-env)`` This library is similar to `env-var`, it just uses one function to parse an environment variable with options for validating the value. But it hasn't been updated in a while and doesn't look very widely used.
- `ts-app-env`  
 This library looks exactly like `@velsa/ts-env`, but maybe a little more up-to-date. The documentation is a little lacking, though.
- `ts-dotenv`  
 This library wraps the dotenv library with the ability to type information with a singular schema object. It seems excellent, but if you don't use .env files in production, it may add an unnecessary step.

* * *

I hope this post helps you find sanity in using environment variables in your Typescript application.

Let me know if you have any questions.

You can find me on [Twitter](https://twitter.com/codingmatty) or email me at [codingmatty@gmail.com](mailto:codingmatty@gmail.com).