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
}
}
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
}
}
Share
Improve this question
asked Apr 20, 2020 at 10:11
user12694034user12694034
6 Answers
Reset to default 5In 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
.
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>,
) {}
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
}
}
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 {}
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}`)
}
}
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];
}
}