user12694034
user12694034

Reputation:

NestJS and typescript config strong types?

I have a main config in my app, which is expressed trough environment variables (process.env). How can i expose it with next JS as one object? In the example below, i can get value by the key. But i passing a string, no typescript comes into play 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 highlighting properties avaliable on this object via typescript
  }

}

Upvotes: 7

Views: 7018

Answers (6)

iserdmi
iserdmi

Reputation: 103

Just create you own service wrapped over default and use it:

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];
  }
}

Upvotes: 0

Felix Hagspiel
Felix Hagspiel

Reputation: 2937

You can pass a typescript type object on the class level.

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

// add this type with your allowed values
type MyType = {
  hello: string;
  someOtherValue: string;
};

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

  getHello(): string {
    return this.configService.get<string>('hello1'); // this should now show an TS error
  }
}

Upvotes: 2

zbr
zbr

Reputation: 7037

In your class's constructor, as you inject ConfigService, parametrize it with the interface representing the shape of your config.

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

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

That will change the type of configService.get to ConfigService<MyConfig, false>.get<any>(propertyPath: keyof MyConfig): any. You'll still be passing in strings, but thanks to propertyPath: keyof MyConfig, TypeScript will now check whether the string represents a key of MyConfig.

So if you try to do something like configService.get('nonexistentConfigProp'), you'll get TS2345: Argument of type '"nonexistentConfigProp"' is not assignable to parameter of type 'keyof MyConfig' which is exactly what you want to happen.

Please note that you can also parametrize get further, e.g. get<T> for it to return T | undefined instead of any. If you'd like it to return just T because you have previously validated the config object and you know its properties are defined, you can parametrize ConfigService further with a second boolean parameter called WasValidated, i.e. ConfigService<MyConfig, true>. Then get<T> will return just T.

Upvotes: 5

Nikaple
Nikaple

Reputation: 121

We've developed the nest-typed-config module to solve the issue.

First you need to create your config model:

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

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

Register TypedConfigModule in AppModule:

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

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

And that's it! You can inject Config in any service now:

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

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

    show() {
        // Full TypeScript support
        console.log(`http://${this.config.host}:${this.config.port}`)
    }
}

Upvotes: 1

Gabi Mor
Gabi Mor

Reputation: 192

I went with a slightly more boilerplaty approach of wrapping nest's ConfigService with my own and exporting it as a part of a module (along with other common services I use often in my app), like so:

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

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

  get MY_CONFIG_STRING() {
    // this reads the value from your .env
    return this.configService.get<string>('MY_CONFIG_STRING')!
  }

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

The user side is sweet and simple:

export class DoSomethingService {
  constructor(private configurationService: ConfigurationService) {}

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

}

Just make sure to bootstrap Nest's config service in your ConfigurationService module like so:

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

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

Upvotes: 1

kaznovac
kaznovac

Reputation: 1582

see https://docs.nestjs.com/techniques/configuration#configuration-namespaces

e.g.

config/database.config.ts JS

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

and inject typed object

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

Upvotes: 4

Related Questions