DEV Community

Taehyuk Han
Taehyuk Han

Posted on

Typed ConfigService in NestJS

Preview

  • Strongly Typed argument(property path) for Nested Configuration Object

typed property path

  • Dynamic return type according to config variables

number-type config variable

string-type config variable

Introduction

In NestJS, ConfigModule with ConfigService helps store and load configuration variables in .env file for convenience instead of using process.env.

Using the standard constructor injection, loading the variables with ConfigService would be like:

// config/configuration.ts
export default () => ({
    port: parseInt(process.env.PORT, 10) || 3000,
    database: {
      host: process.env.DATABASE_HOST || 'localhost',
      port: parseInt(process.env.DATABASE_PORT, 10) || 5432
    }
});
Enter fullscreen mode Exit fullscreen mode
// app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({
      load: [configuration],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode
// app.service.ts
@Injectable()
export class AppService {
  constructor(
    private readonly configService: ConfigService
  ) {}

    // somewhere in the code
    something(){
        // get a custom configuration value
        const dbHost = this.configService.get<string>('database.host'); 
    }
}
Enter fullscreen mode Exit fullscreen mode

Problems

However, this.configService.get just like the above code may not be enough for us using typescript.

  1. There is no automatic “Type Hinting” for this.configService.get. NestJS provides a useful feature of type hinting by passing the type as shown above(this.configService.get<string>), but it may be troublesome to deliver the type at every variable.
  2. “Type Hinting” does not guarantee the actual type of config variables. Since it is just type hinting, this.configService.get<number>('[database.host](http://database.host)'); does not show any errors even though database.host is a string type.

    const dbHost = this.configService.get<number>('database.host');
    
    // not showing type error
    const numVar: number = dbHost;
    
  3. The argument propertyPath can by any string value. The parameter of this.configService.get, propertyPath is a string type, which means that there is no checking for whether given property path is actually in the configuration object or not.

    // not showing type error
    const dbHost = this.configService.get<string>('host');
    
    // undefined
    console.log(dbHost)
    

Solution

So here I am documenting my approach for resolving the problems mentioned above.

Step 1.

First is to make interface and give type for the custom configuration.

// config/configuration.ts
export interface EnvironmentVariables {
    port: number;
    database: {
        host: string;
        port: number;
    }
}

export default (): EnvironmentVariables => ({
    port: parseInt(process.env.PORT, 10) || 3000,
    database: {
      host: process.env.DATABASE_HOST || 'localhost',
      port: parseInt(process.env.DATABASE_PORT, 10) || 5432
    }
});
Enter fullscreen mode Exit fullscreen mode

By doing so, we can check the config variables and their types registered in the ConfigModule.

Step 2.

// config/configuration.ts
export type Leaves<T> = T extends object
  ? {
      [K in keyof T]: `${Exclude<K, symbol>}${Leaves<T[K]> extends never
        ? ''
        : `.${Leaves<T[K]>}`}`;
    }[keyof T]
  : never;

export type LeafTypes<T, S extends string> = S extends `${infer T1}.${infer T2}`
  ? T1 extends keyof T
    ? LeafTypes<T[T1], T2>
    : never
  : S extends keyof T
  ? T[S]
  : never;
Enter fullscreen mode Exit fullscreen mode

In order to receive a definite property path from the nested configuration object, only the leaf paths must be received, so the custom type called Leaves is required. This code was referenced at https://stackoverflow.com/questions/58434389/typescript-deep-keyof-of-a-nested-object .

Then the second custom type called LeafTypes is needed for automatically importing the types of leaf paths.

Step 3.

// config/typed-config.service.ts
@Injectable()
export class TypedConfigService {
  constructor(private configService: ConfigService) {}

  get<T extends Leaves<EnvironmentVariables>>(propertyPath: T): LeafTypes<EnvironmentVariables, T> {
    return this.configService.get(propertyPath);
  }
}
Enter fullscreen mode Exit fullscreen mode
// config/typed-config.module.ts

// @Global() if needed
@Module({
  imports: [
    ConfigModule.forRoot({
      load: [configuration],
    }),
  ],
  providers: [TypedConfigService],
  exports: [TypedConfigService],
})
export class TypedConfigModule {}
Enter fullscreen mode Exit fullscreen mode

Because the ConfigService cannot be used as it is, I created a new service TypedConfigService. The get method was created in the same way as ConfigService, and T was set as Leaves<Environment Variables> to guarantee the type of propertyPath, and return the type accordingly.

Of course, you can override all methods by implementing or extending the ConfigService.

Don't forget to import the ConfigModule to inject the ConfigService from TypedConfigService.

Step 4.

// app.module.ts
@Module({
  imports: [
    TypedConfigModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Finally, we need to import its containing module TypedConfigModule to the module that we want to use config variables.

Usage

Now the strongly typed code is available!

We can use the new service as we used the ConfigService before.

// app.service.ts
@Injectable()
export class AppService {
  constructor(
    private readonly configService: TypedConfigService // ConfigService
  ) {}

    // somewhere in the code
    something(){
        // get a custom configuration value
        const dbHost = this.configService.get('database.host'); 
    }
}
Enter fullscreen mode Exit fullscreen mode

typed property path

number-type config variable

string-type config variable

Summary

The ConfigService currently provided by NestJS has something to be desired related to the type.

However, by using the custom types and the new service that once wraps the existing ConfigService, we expect to be able to utilize more convenient typescript code with strongly typed config variables.

Thank you so much for reading this post and I hope you found this useful for your great applications!

Any better code feedback and questions would be welcome! Thank You :) 🖖

Top comments (1)

Collapse
 
paddingtonthebear profile image
PaddingtonTheBear

Doesn't cover situations where you have nested objects in your config service and just want to grab that entire nested object rather than the full path to a prop in that nested object.