Skip to content

Commit

Permalink
Merge pull request #676 from micalevisk/feat-issue-668
Browse files Browse the repository at this point in the history
feat: let `ConfigService` type know about schema validation
  • Loading branch information
kamilmysliwiec authored Nov 3, 2021
2 parents d795449 + 87972b3 commit af6a374
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 3 deletions.
20 changes: 17 additions & 3 deletions lib/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ import {
} from './config.constants';
import { NoInferType, Path, PathValue } from './types';

/**
* `ExcludeUndefinedIf<ExcludeUndefined, T>
*
* 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> : T | undefined;

export interface ConfigGetOptions {
/**
* If present, "get" method will try to automatically
Expand All @@ -19,7 +30,10 @@ export interface ConfigGetOptions {
}

@Injectable()
export class ConfigService<K = Record<string, unknown>> {
export class ConfigService<
K = Record<string, unknown>,
WasValidated extends boolean = false,
> {
private set isCacheEnabled(value: boolean) {
this._isCacheEnabled = value;
}
Expand All @@ -42,7 +56,7 @@ export class ConfigService<K = Record<string, unknown>> {
* based on property path (you can use dot notation to traverse nested object, e.g. "database.host").
* @param propertyPath
*/
get<T = any>(propertyPath: keyof K): T | undefined;
get<T = any>(propertyPath: keyof K): ExcludeUndefinedIf<WasValidated, 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").
Expand All @@ -52,7 +66,7 @@ export class ConfigService<K = Record<string, unknown>> {
get<T = K, P extends Path<T> = any, R = PathValue<T, P>>(
propertyPath: P,
options: ConfigGetOptions,
): R | undefined;
): ExcludeUndefinedIf<WasValidated, R>;
/**
* 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").
Expand Down
5 changes: 5 additions & 0 deletions tests/jest-e2e.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"globals": {
"ts-jest": {
"tsconfig": "./tests/tsconfig.json"
}
},
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
Expand Down
29 changes: 29 additions & 0 deletions tests/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof databaseConfig> & {
driver: ConfigType<typeof nestedDatabaseConfig>
};
};

@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<Config, true>,
@Optional()
@Inject(databaseConfig.KEY)
private readonly dbConfig: ConfigType<typeof databaseConfig>,
) {}

/**
* 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<Config['database']>('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,
Expand Down
14 changes: 14 additions & 0 deletions tests/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "../",
"strictNullChecks": true
},
"include": [
"**/*.spec.ts",
],
"exclude": [
"node_modules",
"dist"
]
}

0 comments on commit af6a374

Please sign in to comment.