From f6d4b6b08eeb3dacd5c747cbf2fc46eea7ca15fd Mon Sep 17 00:00:00 2001 From: "Micael Levi (lab)" Date: Thu, 23 Sep 2021 10:44:03 -0400 Subject: [PATCH 1/3] feat(): let `ConfigService` type know about schema validation Closes #668 --- lib/config.service.ts | 11 +++++++---- lib/types/exclude-undefined-if.type.ts | 10 ++++++++++ lib/types/index.ts | 1 + 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 lib/types/exclude-undefined-if.type.ts diff --git a/lib/config.service.ts b/lib/config.service.ts index 3c249c4b..734c0a8f 100644 --- a/lib/config.service.ts +++ b/lib/config.service.ts @@ -7,7 +7,7 @@ import { CONFIGURATION_TOKEN, VALIDATED_ENV_PROPNAME, } from './config.constants'; -import { NoInferType, Path, PathValue } from './types'; +import { NoInferType, Path, PathValue, ExcludeUndefinedIf } from './types'; export interface ConfigGetOptions { /** @@ -19,7 +19,10 @@ export interface ConfigGetOptions { } @Injectable() -export class ConfigService> { +export class ConfigService< + K = Record, + WasValidated extends boolean = false, +> { private set isCacheEnabled(value: boolean) { this._isCacheEnabled = value; } @@ -42,7 +45,7 @@ export class ConfigService> { * based on property path (you can use dot notation to traverse nested object, e.g. "database.host"). * @param propertyPath */ - get(propertyPath: keyof K): T | undefined; + get(propertyPath: keyof K): ExcludeUndefinedIf; /** * 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"). @@ -52,7 +55,7 @@ export class ConfigService> { get = any, R = PathValue>( propertyPath: P, options: ConfigGetOptions, - ): R | undefined; + ): ExcludeUndefinedIf; /** * 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"). diff --git a/lib/types/exclude-undefined-if.type.ts b/lib/types/exclude-undefined-if.type.ts new file mode 100644 index 00000000..645eb0ec --- /dev/null +++ b/lib/types/exclude-undefined-if.type.ts @@ -0,0 +1,10 @@ +/** + * `ExcludeUndefinedIf + * + * If `ExcludeUndefined` is `true`, remove `undefined` from `T`. + * Otherwise, constructs the type `T` with `undefined`. + */ +export type ExcludeUndefinedIf< + ExcludeUndefined extends boolean, + T, +> = ExcludeUndefined extends true ? Exclude : T | undefined; diff --git a/lib/types/index.ts b/lib/types/index.ts index b39e9456..2be2b7a5 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -2,3 +2,4 @@ export * from './config-object.type'; export * from './config.type'; export * from './no-infer.type'; export * from './path-value.type'; +export * from './exclude-undefined-if.type'; From 259d793e6cf8df0df02dc10a0e2e3c721d56122e Mon Sep 17 00:00:00 2001 From: "Micael Levi (lab)" Date: Thu, 23 Sep 2021 21:42:04 -0400 Subject: [PATCH 2/3] feat(): do not expose the type utility `ExcludeUndefinedIf` --- lib/config.service.ts | 13 ++++++++++++- lib/types/exclude-undefined-if.type.ts | 10 ---------- lib/types/index.ts | 1 - 3 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 lib/types/exclude-undefined-if.type.ts diff --git a/lib/config.service.ts b/lib/config.service.ts index 734c0a8f..b93761cb 100644 --- a/lib/config.service.ts +++ b/lib/config.service.ts @@ -7,7 +7,18 @@ import { CONFIGURATION_TOKEN, VALIDATED_ENV_PROPNAME, } from './config.constants'; -import { NoInferType, Path, PathValue, ExcludeUndefinedIf } from './types'; +import { NoInferType, Path, PathValue } from './types'; + +/** + * `ExcludeUndefinedIf + * + * If `ExcludeUndefined` is `true`, remove `undefined` from `T`. + * Otherwise, constructs the type `T` with `undefined`. + */ +type ExcludeUndefinedIf< + ExcludeUndefined extends boolean, + T, +> = ExcludeUndefined extends true ? Exclude : T | undefined; export interface ConfigGetOptions { /** diff --git a/lib/types/exclude-undefined-if.type.ts b/lib/types/exclude-undefined-if.type.ts deleted file mode 100644 index 645eb0ec..00000000 --- a/lib/types/exclude-undefined-if.type.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * `ExcludeUndefinedIf - * - * If `ExcludeUndefined` is `true`, remove `undefined` from `T`. - * Otherwise, constructs the type `T` with `undefined`. - */ -export type ExcludeUndefinedIf< - ExcludeUndefined extends boolean, - T, -> = ExcludeUndefined extends true ? Exclude : T | undefined; diff --git a/lib/types/index.ts b/lib/types/index.ts index 2be2b7a5..b39e9456 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -2,4 +2,3 @@ export * from './config-object.type'; export * from './config.type'; export * from './no-infer.type'; export * from './path-value.type'; -export * from './exclude-undefined-if.type'; From 87972b3522ad933bc02d5831cb75bd5b5b7f1987 Mon Sep 17 00:00:00 2001 From: "Micael Levi (lab)" Date: Wed, 29 Sep 2021 22:26:25 -0400 Subject: [PATCH 3/3] test: add attempt to test the new feature added Since there's no other way to check TypeScript types within this project without addind some 3rd-party lib, I've made this approach: let TSC complains about typings while running the npm-script `test:integration`. --- tests/jest-e2e.json | 5 +++++ tests/src/app.module.ts | 29 +++++++++++++++++++++++++++++ tests/tsconfig.json | 14 ++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 tests/tsconfig.json diff --git a/tests/jest-e2e.json b/tests/jest-e2e.json index 4ca0d877..4bff75bd 100644 --- a/tests/jest-e2e.json +++ b/tests/jest-e2e.json @@ -1,4 +1,9 @@ { + "globals": { + "ts-jest": { + "tsconfig": "./tests/tsconfig.json" + } + }, "moduleFileExtensions": ["js", "json", "ts"], "rootDir": ".", "testEnvironment": "node", diff --git a/tests/src/app.module.ts b/tests/src/app.module.ts index 25f3d5b1..79e7dfdb 100644 --- a/tests/src/app.module.ts +++ b/tests/src/app.module.ts @@ -7,15 +7,44 @@ import { ConfigService } from '../../lib/config.service'; import databaseConfig from './database.config'; import nestedDatabaseConfig from './nested-database.config'; +type Config = { + database: ConfigType & { + driver: ConfigType + }; +}; + @Module({}) export class AppModule { constructor( private readonly configService: ConfigService, + // The following is the same object as above but narrowing its types + private readonly configServiceNarrowed: ConfigService, @Optional() @Inject(databaseConfig.KEY) private readonly dbConfig: ConfigType, ) {} + /** + * This method is not meant to be used anywhere! It just here for testing + * types defintions while runnig test suites (in some sort). + * If some typings doesn't follows the requirements, Jest will fail due to + * TypeScript errors. + */ + private noop(): void { + // Arrange + const identityString = (v: string) => v; + const identityNumber = (v: number) => v; + // Act + const knowConfig = this.configServiceNarrowed.get('database'); + // Assert + // We don't need type assertions bellow anymore since `knowConfig` is not + // expected to be `undefined` beforehand. + identityString(knowConfig.host); + identityNumber(knowConfig.port); + identityString(knowConfig.driver.host); + identityNumber(knowConfig.driver.port); + } + static withCache(): DynamicModule { return { module: AppModule, diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 00000000..339daa47 --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "../", + "strictNullChecks": true + }, + "include": [ + "**/*.spec.ts", + ], + "exclude": [ + "node_modules", + "dist" + ] +}