Typed ConfigService in NestJS
Published: 2024-01-04 14:24:18

## Preview

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

![typed property path](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/js9fcb8gwk48pb6rvui9.png)

- Dynamic return type according to config variables

![number-type config variable](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k3ar4p5fkxz074fj9hef.png)

![string-type config variable](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/trgr7bvgdoxq8uhh9e16.png)

## 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:

```jsx
// 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
    }
});
```

```jsx
// app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({
      load: [configuration],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

```jsx
// 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');	
	}
}
```

## 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.
    
    ```jsx
    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. 
    
    ```jsx
    // 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.

```jsx
// 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
    }
});
```

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

### Step 2.

```jsx
// 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;
```

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.

```jsx
// 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);
  }
}
```

```jsx
// config/typed-config.module.ts

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

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.

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

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.

```jsx
// 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');	
	}
}
```

![typed property path](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/js9fcb8gwk48pb6rvui9.png)

![number-type config variable](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k3ar4p5fkxz074fj9hef.png)

![string-type config variable](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/trgr7bvgdoxq8uhh9e16.png)


## 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 :) 🖖