Check env vars at runtime, with full TypeScript support

20/10/2024

As developers, we inevitably work with environment variables. When using TypeScript, this can become a headache because you always have to check if a variable exists and handle its type accordingly.

While there are libraries that provide schema-based validation and type inference for process.env or .env files, they don’t always fit every use case, especially when working with Lambda functions in a serverless project.

Let me explain the problem

When working with Lambda functions, your environment variables may be populated during stack deployment, based on the resources in the stack, like DynamoDB TableName or SQS QueueUrl.

Defining a schema for each of your lambda handlers, even if you only need one env variable seems a bit too much for me.

The Solution

Here’s a small utility function that checks environment variables at runtime while providing full TypeScript support. It allows you to define required and optional variables, handle default values, and infer the correct types from your environment configuration.

!Important: This function uses const modifiers on type parameters so you must use TypeScript 5.0 or above.

// checkEnv.ts

type EnvVarWithOptions = {
  name: string;
  default?: string;
  optional?: boolean;
};

type EnvVar = string | EnvVarWithOptions;

type Names<T> = T extends Array<infer N>
  ? N extends string
    ? N
    : N extends EnvVarWithOptions
    ? N["name"]
    : never
  : never;

type ValueType<T, K> = T extends Array<infer N>
  ? N extends K
    ? string
    : N extends EnvVarWithOptions
    ? N["name"] extends K
      ? N["optional"] extends true
        ? string | undefined
        : string
      : never
    : never
  : never;

export function checkEnv<const T extends Array<EnvVar>>(
  ...params: T
): { [K in Names<T>]: ValueType<T, K> } {
  const env = {} as { [K in Names<T>]: ValueType<T, K> };

  for (const param of params) {
    if (typeof param === "string") {
      if (!Object.hasOwn(process.env, param)) {
        throw new Error(`${param} not available in process.env.`);
      }

      Object.assign(env, { [param]: process.env[`${param}`] });
    } else {
      const { name, default: defaultValue, optional } = param;

      if (
        !Object.hasOwn(process.env, name) &&
        typeof defaultValue !== "string" &&
        optional !== true
      ) {
        throw new Error(`${name} not available in process.env.`);
      }

      Object.assign(env, {
        [name]: process.env[`${name}`] ?? defaultValue ?? undefined,
      });
    }
  }

  return env;
}

How to Use It

Here’s how you can use the checkEnv function to validate and infer types for your environment variables:

import { checkEnv } from “./checkEnv”;

const env = checkEnv(
  // this is required. will throw an error if not found.
  "FIRST_ENV_VAR",
  {
    // this is optional. will return the `default` value if not found
    name: "SECOND_ENV_VAR",
    default: "default_value",
  },
  {
    // this is optional.
    name: "THIRD_ENV_VAR",
    optional: true,
  }
);

/**
 * The type for the env object will be:
 * {
 *   FIRST_ENV_VAR: string;
 *   SECOND_ENV_VAR: string;
 *   THIRD_ENV_VAR: string | undefined;
 * }
 */

With less than 30 lines of code (excluding types), this utility function helps you safely check and infer types for your environment variables at runtime. No need for schemas or external libraries. It’s lightweight and perfect for Lambda functions.

Happy coding!