Ensuring that all environmental variables are properly set in Typescript by utilizing interfaces and resolving union to tuple type discrepancies

In order to create a required application env variables file named env.d.ts, I want to ensure that any modifications or additions to it will trigger TypeScript errors and runtime errors for the checkEnv function if a value is not set.

To achieve this, I have created a top-level file called env.d.ts to extend the process.env:

declare global {
    namespace NodeJS {
        interface ProcessEnv {
            PORT: string;
            HOST: string;
        }
    }
}
export { }; // necessary for declarations to work

I then include this file in the tsconfig.json:

{
  "include": [
    "./env.d.ts"
  ]
}

While looking for a workaround due to TypeScript's union to tuple type limitation, I discovered this solution. Is there an easier way to handle this?

Typescript sandbox

// node process types
interface ProcessEnv {
    [key: string]: string | undefined
    TZ?: string;
}
declare var process: {
    env: ProcessEnv
}

// app env types from `env.d.ts`
interface ProcessEnv {
    PORT: string;
    HOST: string;
    // Ensure `checkEnv` throws error if something new is added to `env.d.ts`
}

type RemoveIndexSignature<ObjectType> = {
    [KeyType in keyof ObjectType as {} extends Record<KeyType, unknown>
    ? never
    : KeyType]: ObjectType[KeyType];
};

class UnreachableCaseError extends Error {
  constructor(unrechableValue: never) {
    super(`Unreachable case: ${unrechableValue}`);
  }
}

function checkEnv() {
    type AppEnv = Exclude<keyof RemoveIndexSignature<typeof process.env>, 'TZ'>  // PORT | HOST
    const envKeys: AppEnv[] = [
        'HOST',
        'PORT'
        // 'X' error - nice
        // no error if something will be added to `env.d.ts`
    ];

    for (const envKey of envKeys) {
        switch (envKey) {
            case 'HOST':
            case 'PORT': {
                if (process.env[envKey] === undefined) {
                    throw new Error(`Env variable "${envKey}" not set`);
                }
                break;
            }
            default:
                throw new UnreachableCaseError(envKey);
        }
    }
}
checkEnv();

Answer №1

Perhaps simplifying by utilizing an enum could be more efficient? The enum values are beneficial for both compile-time typing and runtime usage.

enum EnvVariables {
    PORT = 'PORT',
    HOST = 'HOST'
}

This approach allows us to incorporate both PORT and HOST into the ProcessEnv interface.

type EnvVariablesType = {
    [K in keyof typeof EnvVariables]: string
}

declare global {
    namespace NodeJS {
        interface ProcessEnv extends EnvVariablesType {}
    }
}

Rather than maintaining an array of all environment variables, we can simply use Object.values to extract all enum keys.

const envKeys = Object.values(EnvVariables).filter(value => typeof value === 'string') as (keyof typeof EnvVariables)[];

The presence of a switch statement seems extraneous.

for (const envKey of envKeys) {        
  if (process.env[envKey] === undefined){
    throw new Error(`Env variable "${envKey}" not set`);
  }
}

Please confirm if this solution meets your requirements.

Playground


Edit:

Upon further consideration: Why not take a simpler approach and utilize an array instead?

// env.d.ts

const envVariables = ['HOST', 'PORT'] as const

type EnvVariablesType = {
    [K in typeof envVariables[number]]: string
}

declare global {
    namespace NodeJS {
        interface ProcessEnv extends EnvVariablesType {}
    }
}

// other file

function checkEnv() {
    for (const envKey of envVariables) {        
        if (process.env[envKey] === undefined){
            throw new Error(`Env variable "${envKey}" not set`);
        }
    }
}
checkEnv();

Playground

Answer №2

Here is a specialized solution based on types!

To make it work smoothly, an auxiliary function is needed to handle certain aspects for you. Nonetheless, I trust that this additional step won't pose a major inconvenience.

type HasAll<T extends ReadonlyArray<string>> = [T[number]] extends [AppEnv] ? [AppEnv] extends [T[number]] ? T : never : never;

This code assesses if the array T contains exactly the items specified in the union AppEnv using conditionals. The presence of [] ensures that the conditional remains non-distributive and does not turn into a raw type.

We utilize the HasAll validator within this utility function:

function hasAll<T extends ReadonlyArray<string>>(t: HasAll<T>): T { return t as T; }

TypeScript exhibits intelligence by deducing the type T and passing it to HasAll. If it meets the criteria, then the parameter t will be of type T, otherwise it will be never.

This methodology leads to robust type checking:

hasAll(["HOST"] as const); // error
hasAll(["HOST", "PORT"] as const); // fine
hasAll(["HOST", "PORT", "X"] as const); // error

However, one drawback is that it does not detect duplicates:

hasAll(["HOST", "PORT", "HOST"] as const); // fine

While identifying duplicates is feasible, implementing it would require excessive complexity and effort for such a minor issue. Therefore, I hope this limitation is acceptable.

You can observe how this solution functions with your specific scenario below:

Access Playground

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Guide on establishing the structure of a map data type in Dart

When working with typescript: const someMap = {a: "hi", b: 3}; // Typescript can determine the type of someMap as {a:string, b:number}. const greet = someMap.a; // Typescript identifies greet as a string. const someMapList: {a:string, b:number} ...

A step-by-step guide on customizing the background color of a Dialog in Angular Material (Version 16)

I've been attempting to modify the background color of my Angular Material Dialog by utilizing the panelClass property in the MatDialogConfig. Unfortunately, I'm encountering a partial success. I am aiming to set the background color as red (jus ...

Universal Parameter Typing in Functions

I'm grappling with a concept that seems obvious to me, yet is disallowed by Typescript when all strict flags are enabled (presumably for valid reasons). Let me illustrate: We all understand the following: export interface Basic { value: "foo&q ...

Utilize @db.Decimal within the Prisma framework for the parameters "s", "e", and "d"

When I define the schema in Prisma like this: value Decimal? @db.Decimal(7, 4) Why do I always get this format when retrieving the value from the database: "value": { "s": 1, "e": 0, & ...

Looking to organize a table in jhipster but unsure of the process. Can someone provide guidance on how

For the past week, I have been delving into jhipster and attempting to incorporate a sortable table. Could someone clarify how exactly the jhiSort and jhiSortBy functions operate? I am struggling to grasp the purpose of the predicate, ascending, and call ...

What are the steps to combine two collections using rxjs?

I need to combine two collections (tokens and trends) based on their IDs, where each item in the result should include data from both collections. This means that the ID of an item in the trends collection matches the ID of the corresponding item in the to ...

Encountering an error in Angular where the property does not exist in type

Struggling to create a collapsible menu within my header component in an Angular project, I've hit a snag with proper JSON formatting. The error message that keeps popping up reads: Error: src/app/components/header/header.component.html:48:49 - error ...

Managing asset paths post ng build: A guide

I've been attempting to use assets to display svg icons on my ESRI map. I'm working with Angular9 and the esri js api, trying to add a symbol from a URL. Locally, the svg appears on the map, but once I build and deploy the project to IIS, it sta ...

The promise object is displayed instead of the actual data retrieved from the API call

I am currently working on fetching data from an API and showcasing the name of the returned data on the front end. This function successfully retrieves the data through an API call: async function retrieveData(url){ var _data; let response = await fetch( ...

What could be the reason behind TS showing the error "Type 'MyMedicine[]' cannot be assigned to type 'MyMedicine' as a parameter"?

Here is an interface I have created: export interface MyMedicine { _id: String; name: String; quantity: Number; time: String; } This snippet shows my Angular service used for posting data: postMed(newMed): Observable<MyMedicine[]>{ var he ...

What led the Typescript Team to decide against making === the default option?

Given that Typescript is known for its type safety, it can seem odd that the == operator still exists. Is there a specific rationale behind this decision? ...

Adding input fields dynamically

I have a component named home with 3 input fields for Country, City, and State. You can see them in the image below: https://i.sstatic.net/ZMXfD.png I've implemented dynamic addition of input fields, and it's functioning well as shown in the im ...

Error in Typescript Observable when using .startWith([])

I encountered the TypeScript error below: Error:(34, 20) TS2345: Argument of type 'undefined[]' is not assignable to parameter of type 'number | Scheduler'. Type 'undefined[]' is not assignable to type 'Scheduler& ...

Uncovering the Secrets of Typescript Mixins: Leveraging Shared Properties Across Combined Classes

I am currently exploring the concept of mixins as explained in the TypeScript documentation. https://www.typescriptlang.org/docs/handbook/mixins.html You can find a playground setup here. My question revolves around defining functions like jump() and du ...

Weird occurrences in Typescript generics

function resizeImage<T extends File | Blob>(input: T, width: number, height: number): Promise<T> { return Promise.resolve(new File([new Blob()], 'test.jpg')) } Error: (48, 3) TS2322:Type 'Promise' is not assignable to ...

I am having trouble locating my TypeScript package that was downloaded from the NPM registry. It seems to be showing as "module not found"

Having some challenges with packaging my TypeScript project that is available on the npm registry. As a newcomer to module packaging for others, it's possible I've made an error somewhere. The following sections in the package.json appear to be ...

Tips for managing onChange events in TypeScript

I'm still learning Typescript and I have a question regarding handling the onChange event in a TextField component when using Typescript. Can you provide guidance on how to approach this? I currently have a function called handleChangeDate(e: React. ...

Runtime error: Angular property is null

I've set up the following structure: export class HomePageComponent implements OnInit { constructor(private httpClient: HttpClient) { } nummer: FormControl = new FormControl("", this.nummerValidator()); firstname: FormControl = new FormContr ...

Having trouble with filtering an array using the some() method of another array?

When utilizing the code below, my goal is to filter the first array by checking if the item's id exists in the second array. However, I am encountering an issue where the result is coming back empty. dialogRef.afterClosed().subscribe((airlines: Airli ...

ngx-emoji mart - The error message "Type 'string' is not assignable" is being displayed

While working on a project involving the @ctrl/ngx-emoji-mart package, I encountered a perplexing issue. The code functioned flawlessly in Stackblitz but when I attempted to run it on my local system, an error surfaced: Type 'string' is not assig ...