Is NestJS the best choice for enforcing strong typing with TypeScript configurations?

My app has a main configuration expressed through environment variables (process.env). How can I expose it as one object using Next.js? In the code example below, I am able to retrieve values by keys. However, since I am passing a string, TypeScript is not utilized here.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config';
import { envVarsValidator } from "../interfaces/Config";

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: envVarsValidator,
    })
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

import { Injectable } from '@nestjs/common';
import { ConfigService } from "@nestjs/config";

@Injectable()
export class AppService {
  constructor(private configService: ConfigService) {}

  getHello(): string {
    return this.configService.get<string>('hello'); // not what I need;
  }

}

Pseudocode for what I need:


export class SomeService {
  constructor(private configService: ConfigService) {}

  someLogic(): any {
    const port = this.configService.config.port;
// What I need is one main config object with highlighted properties available on this object via TypeScript
  }

}

Answer №1

When constructing your class, be sure to inject ConfigService and parameterize it with the interface that defines your configuration structure.

interface MyConfig {
  port: number;
  dbConnectionString: string;
}

class AppService {
  constructor(private configService: ConfigService<MyConfig>) {}
}

This will update the type of configService.get to

ConfigService<MyConfig, false>.get<any>(propertyPath: keyof MyConfig): any
. Even though you continue to pass in strings, TypeScript will now verify if the string matches a key in MyConfig.

For instance, calling

configService.get('nonexistentConfigProp')
will result in
TS2345: Argument of type '"nonexistentConfigProp"' is not assignable to parameter of type 'keyof MyConfig'
, which is the desired behavior.

Additionally, you can further customize the get method, such as using get<T> to return T | undefined instead of any. If you have already validated the config object and want get<T> to only return T, you can introduce another boolean parameter named WasValidated when parameterizing ConfigService like this:

ConfigService<MyConfig, true>
.

Answer №2

check out the link for more information on configuration namespaces: https://docs.nestjs.com/techniques/configuration#configuration-namespaces

for example:

config/database.config.ts (JavaScript)

export default registerAs('database', () => ({
  host: process.env.DATABASE_HOST,
  port: process.env.DATABASE_PORT || 5432
}));

then you can inject a typed object like this:

constructor(
  @Inject(databaseConfig.KEY)
  private dbConfig: ConfigType<typeof databaseConfig>,
) {}

Answer №3

You have the ability to define a TypeScript type object at the class level.

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

// Define a custom type with specified values
type MyType = {
  greeting: string;
  anotherValue: string;
};

@Injectable()
export class AppService {
  constructor(private configService: ConfigService<MyType>) {} // Specify the type here

  getGreeting(): string {
    return this.configService.get<string>('hello1'); // This will now trigger a TypeScript error
  }
}

Answer №4

For my project, I decided to take a more boilerplate approach by creating a custom wrapper for Nest's ConfigService. This wrapper is then exported as part of a module that includes other commonly used services in my application. Here's how I set it up:

import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'

@Injectable()
export class CustomConfigurationService {
  constructor(private configService: ConfigService) {}

  get MY_CONFIG_STRING() {
    // Retrieve value from .env file
    return this.configService.get<string>('MY_CONFIG_STRING')!
  }

  get MY_CONFIG_Number() {
    // Retrieve value from .env file
    return this.configService.get<number>('MY_CONFIG_NUMBER')!
  }
}

Using the custom configuration service on the user side is quite straightforward:

export class DoSomethingService {
  constructor(private customConfigService: CustomConfigurationService) {}

  doSomething() {
    console.log(this.customConfigService.MY_CONFIG_VALUE)
  }

}

To ensure everything works smoothly, don't forget to include Nest's config service in your ConfigurationService module like this:

import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { CustomConfigurationService } from './custom-configuration.service'

@Module({
  imports: [ConfigModule.forRoot()],
  providers: [
    ConfigService,
    CustomConfigurationService,
  ],
  exports: [CustomConfigurationService],
})
export class SharedModule {}

Answer №5

We have come up with the solution by introducing the nest-typed-config module.

To begin, you will need to define your configuration model:

// config.ts
export class Configuration {
    @IsString()
    public readonly host!: string;

    @IsNumber()
    public readonly port!: number;
}

Include the TypedConfigModule in your AppModule:

// app.module.ts
import { Module } from '@nestjs/common';
import { TypedConfigModule, fileLoader } from 'nest-typed-config';
import { AppService } from './app.service';
import { Configuration } from './config';

@Module({
    imports: [
        TypedConfigModule.forRoot({
            schema: Configuration,
            load: fileLoader(),
            isGlobal: true,
        }),
    ],
    providers: [AppService],
})
export class AppModule {}

That's all! You can now inject Configuration into any service:

// app.service.ts
import { Configuration } from './config';

@Injectable()
export class AppService {
    constructor(private readonly config: Configuration) {}

    display() {
        // Enjoy full TypeScript support
        console.log(`http://${this.config.host}:${this.config.port}`)
    }
}

Answer №6

To enhance the default service, you can create your own custom service like this:

import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { ConfigSchema } from "src/other/configuration";

@Injectable()
export class TypedConfigService {
  constructor(private configService: ConfigService<ConfigSchema>) {}

  get<T extends keyof ConfigSchema>(key: T) {
    return this.configService.get(key) as ConfigSchema[T];
  }
}

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

Struggling to maintain consistent updates on a child element while using the @Input property

I need to ensure that the data source in loans.component.ts is updated whenever a new loan is submitted from loan-form.component.ts. Therefore, in loan-form.component.ts, I have the following function being called when the form is submitted: onSubmit() { ...

Error message: Angular JS function is not defined

I have been diving into AngularJS with the help of video tutorials from AngularJS.org. However, when I try to run my code, an error message pops up: Error: [ng:areq] Argument 'ReviewController' is not a function Even though my code mirrors th ...

Creating a custom HTTP interceptor that can intercept a response and handle errors within the 'response' property

My setup includes a basic Angular http interceptor designed to manage errors. I need to verify if the received data is categorized as a string, treating it as an error rather than a success. 'response': function(response) { if(typeof respons ...

AngularJS allows for the population of a JSON array from user input using the ng-model directive

I'm currently working on a form where I need to send the data to the backend in JSON format. The challenge I'm facing is that some of the form fields should be used to populate an array. Here's an abridged version of the form (using a Jade ...

How is it possible that this component is able to work with slotting without needing to specify the slot name

I have created two Web Components using Stencil.js: a Dropdown and a Card. Within my Dropdown, the structure is as follows: <div class='dropdown'> <slot name="button"/> <slot/> </div> The nested chil ...

When setupFilesAfterEnv is added, mock functions may not function properly in .test files

Upon including setupFilesAfterEnv in the jest.config.js like this: module.exports = { preset: 'ts-jest', testEnvironment: 'node', setupFilesAfterEnv: ["./test/setupAfterEnv.ts"] } The mock functions seem to sto ...

JavaScript does not recognize jsPDF

After importing the jsPDF library, I attempted to export to PDF but encountered a JavaScript error stating that jsPDF is not defined. I tried various solutions from similar posts but none of them seemed to work for me. You can find the fiddle here: https ...

What is the reason why the VIEW SOURCE feature does not display filled dropdown list boxes?

The following code functions well in terms of dynamically populating two dropdown list boxes. Initially, the getBuildings function is called to make a request to the getBuildings.php file. This action fills the buildingID dropdown list box with options. ...

preventing sliding in a specific angle within the angularjs ionic framework

I am completely new to angularjs and embarking on my very first app creation journey with the ionic framework. The initial step I took was to generate an ionic app using this command: $ ionic start myApp sidemenu The creation of the app went smoothly, co ...

Is e.preventDefault() failing to work in Next.js?

Hey everyone, I'm new to react.js and next.js. I attempted to copy some data from a form and display it in the console, but using e.preventDefault() is preventing me from submitting my form. Is there a more effective way to achieve what I'm aimin ...

"Exploring the Constraints of MongoDB's Range Functionality

I am facing an issue with my nodejs and mongodb setup. I am trying to search for documents within a specific number range but the function is returning results outside of that range. Here is an example of my code where I am attempting to retrieve documents ...

Maintain the spacing of an element when utilizing *ngFor

Using Angular.js and *ngFor to loop over an array and display the values. The goal is to preserve the spaces of elements in the array: string arr1 = [" Welcome Angular ", "Line1", "Line2", " Done "] The ...

Using jQuery to assign the value of a hidden element to data being posted

When using jQuery's post() method to call an ajax action that returns JSON data ({"Success": "true" } or {"Success": "false"}), the value of a hidden HTML element is supposed to be set to the Success value in the returned object. However, after settin ...

How to Properly Manipulate DOM Elements in an Angular Directive

My directive, powerEntry, has different CSS classes that I want to add or remove based on the model state. Currently, in my link function, I have some logic like this: Script.JS if (calcState.availablePoints > 0 && isHighEnoughLevel) { ...

What is the initial component that Angular will activate or run?

Uncertainty surrounds the correctness of my question. Within my application, there exist numerous routing components and feature modules. I am eager to discern whether the appcomponent.ts file or the routing logincomponent.ts file will be executed first. ...

Fresh class retains the characteristics of the former class

As I navigate my way through learning jQuery, I've encountered a puzzling issue that I can't seem to solve. No existing articles seem to address my specific problem, so I am turning to this platform in hopes of finding a solution. I'm puzz ...

Issues arise when the Angular controller fails to load

I'm experiencing an issue with my Angular controller where the code inside its constructor is not running. Here's a snippet of the relevant pieces: conversationcontrollers.js: var exampleApp = angular.module('exampleApp',[]); console ...

Updating Reference Models in Mongoose: A Simple Guide to Updating a Single Model

I have a scenario involving two models: Skill and Course. The challenge I face is ensuring that whenever I update the name of an existing skill, it should also automatically update the corresponding skill name within the course object where this skill is u ...

What could be causing the cleaning command to malfunction in a React Native project managed by Gradle?

After navigating to the android directory using cd .\android, I attempted to clean Gradle files without success. The command used is shown below. D:\ReactNativeVS\demo\android>./gradlew clean However, the following error message ap ...

Tips for making my JavaScript form open a new window after submitting

Currently, the link opens in the same window when submitted. I would like it to open in a new window instead. This is the script in the head section: <script type="text/javascript"> function getURL(val){ base = 'http://www.domain.com/'; ...