Skip to content

Commit

Permalink
Merge pull request #492 from nestjs/feat/infer-dot-notation
Browse files Browse the repository at this point in the history
feat(): support inferring types from dot notation
  • Loading branch information
kamilmysliwiec committed Feb 9, 2021
2 parents 6f0fb40 + fd5d40a commit 12ac723
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 38 deletions.
2 changes: 1 addition & 1 deletion lib/config.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export class ConfigModule {
provide: ConfigService,
useFactory: (configService: ConfigService) => {
if (options.cache) {
configService.isCacheEnabled = true;
(configService as any).isCacheEnabled = true;
}
return configService;
},
Expand Down
53 changes: 43 additions & 10 deletions lib/config.service.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
import { Inject, Injectable, Optional } from '@nestjs/common';
import { isUndefined } from '@nestjs/common/utils/shared.utils';
import get from 'lodash.get';
import has from 'lodash.has';
import set from 'lodash.set';
import { isUndefined } from 'util';
import {
CONFIGURATION_TOKEN,
VALIDATED_ENV_PROPNAME,
VALIDATED_ENV_PROPNAME
} from './config.constants';
import { NoInferType } from './types';

export type PathImpl<T, Key extends keyof T> =
Key extends string
? T[Key] extends Record<string, any>
? | `${Key}.${PathImpl<T[Key], Exclude<keyof T[Key], keyof any[]>> & string}`
| `${Key}.${Exclude<keyof T[Key], keyof any[]> & string}`
: never
: never;

export type PathImpl2<T> = PathImpl<T, keyof T> | keyof T;
export type Path<T> = PathImpl2<T> extends string | keyof T ? PathImpl2<T> : keyof T;

export type PathValue<T, P extends Path<T>> =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? Rest extends Path<T[Key]>
? PathValue<T[Key], Rest>
: never
: never
: P extends keyof T
? T[P]
: never;

@Injectable()
export class ConfigService<K = Record<string, any>> {
get isCacheEnabled(): boolean {
return this._isCacheEnabled;
private set isCacheEnabled(value: boolean) {
this._isCacheEnabled = value;
}

set isCacheEnabled(value: boolean) {
this._isCacheEnabled = value;
private get isCacheEnabled(): boolean {
return this._isCacheEnabled;
}

private readonly cache: Partial<K> = {} as any;
Expand All @@ -43,21 +65,32 @@ export class ConfigService<K = Record<string, any>> {
* @param propertyPath
* @param defaultValue
*/
get<T = any>(propertyPath: keyof K, defaultValue: NoInferType<T>): T;
get<T = K, P extends Path<T> = any, R = PathValue<T, P>>(propertyPath: P, options: { inferDotNotation: true }): R | undefined;
/**
* Get a configuration value (either custom configuration or process environment variable)
* based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
* It returns a default value if the key does not exist.
* @param propertyPath
* @param defaultValue
*/
get<T = any>(propertyPath: keyof K, defaultValue?: T): T | undefined {
get<T = any>(propertyPath: keyof K, defaultValue: NoInferType<T>): T;
/**
* Get a configuration value (either custom configuration or process environment variable)
* based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
* It returns a default value if the key does not exist.
* @param propertyPath
* @param defaultValueOrOptions
*/
get<T = any>(propertyPath: keyof K, defaultValueOrOptions?: T | { inferDotNotation: true }): T | undefined {
const validatedEnvValue = this.getFromValidatedEnv(propertyPath);
if (!isUndefined(validatedEnvValue)) {
return validatedEnvValue;
}
defaultValueOrOptions = (defaultValueOrOptions as { inferDotNotation: true })?.inferDotNotation
? undefined
: defaultValueOrOptions;

const processEnvValue = this.getFromProcessEnv(propertyPath, defaultValue);
const processEnvValue = this.getFromProcessEnv(propertyPath, defaultValueOrOptions);
if (!isUndefined(processEnvValue)) {
return processEnvValue;
}
Expand All @@ -67,7 +100,7 @@ export class ConfigService<K = Record<string, any>> {
return internalValue;
}

return defaultValue;
return defaultValueOrOptions as T;
}

private getFromCache<T = any>(
Expand Down
34 changes: 8 additions & 26 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"url": "https://github.com/nestjs/config#readme",
"scripts": {
"build": "rimraf -rf dist && tsc -p tsconfig.json",
"format": "prettier --write \"{lib,test}/**/*.ts\"",
"lint": "eslint 'lib/**/*.ts' --fix",
"prepublish:npm": "npm run build",
"publish:npm": "npm publish --access public",
Expand Down Expand Up @@ -37,7 +38,7 @@
"@types/lodash.get": "4.4.6",
"@types/lodash.has": "4.5.6",
"@types/lodash.set": "4.3.6",
"@types/node": "7.10.8",
"@types/node": "14.14.25",
"@types/uuid": "8.3.0",
"@typescript-eslint/eslint-plugin": "4.15.0",
"@typescript-eslint/parser": "4.15.0",
Expand Down
11 changes: 11 additions & 0 deletions tests/e2e/optional-generic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ describe('Optional Generic()', () => {
expect(port).toBeTruthy();
});

it(`should infer type from a dot notation`, () => {
const configService = moduleRef.get<
ConfigService<{ obj: { test: boolean } }>
>(ConfigService);

const obj = configService.get('obj', { inferDotNotation: true });
const test = configService.get('obj.test', { inferDotNotation: true });
expect(obj?.test).toBeUndefined();
expect(test).toBeUndefined();
});

it(`should allow any key without a generic`, () => {
const configService = moduleRef.get<ConfigService>(ConfigService);
const port = configService.get('PORT');
Expand Down

0 comments on commit 12ac723

Please sign in to comment.