Readonly ConfigType
Is there an existing issue that is already proposing this?
- [X] I have searched the existing issues
Is your feature request related to a problem? Please describe it
Using Configuration namespaces is pretty powerful as it enhances type safety and reduces amount of boilerplate code. However, configuration object is a mutable singleton shared across the whole application. There are no limitations for mutating configuration object in one service, which may affect other services.
Describe the solution you'd like
Adding readonly config type directly to framework could prevent config mutation errors on compile time and reduce amount of boilerplate code which is similar across projects.
API
There are 3 possible APIs for this:
-
Making
ConfigTypeinfer readonly config type by default. I think it is the best solution, however it introduces a BREAKING CHANGE -
Changing
ConfigTypesignature:// Before type ConfigType<T extends (...args: any) => any> = ... // After type ConfigType<T extends (...args: any) => any, IsReadonly extends boolean = false> = ...This solution doesn't affect existing codebases, however it adds possibility to make
ConfigTypeto infer readonly config type. The API is similar toWasValidatedparam inConfigService. I don't like this solution as adding extra param is a bit confusing in my opinion. -
Introducing a new
ReadonlyConfigTypetype. In my opinion new type is more expressive than the second solution.
Implementation
There are 2 ways to implement readonly config type:
-
Naive
ReadonlyDeep. This implementation is limited as it doesn't narrow arrays for tuples for example and doesn't work with arrays of objects.type ReadonlyDeep<T> = { readonly [P in keyof T]: T[P] extends Record<string, unknown> ? ReadonlyDeep<T[P]> : T[P]; }; -
Recursive
ReadonlyDeepsee type-fest ReadonlyDeep implementation.
What is the motivation / use case for changing the behavior?
.env
PORT=3000
src/config.ts
...
export default registerAs("config", () => ({
port: Number(process.env.PORT),
}));
src/app.service.ts
...
@Injectable()
export class AppService {
constructor(
@Inject(config.KEY) private readonly configuration: ConfigType<typeof config>
) {}
mutateConfig() {
// port now is globally equal to 3001
// No ts errors
this.configuration.port = 3001;
}
}
Adding as const to config registration doesn't solve the issue as ConfigType still infers as a mutable config type:
export default registerAs("config", () => ({
port: Number(process.env.PORT),
} as const));
For now the only way to address this issue is to manually narrow inferred config type to readonly:
type ReadonlyDeep<T> = ...
type Config = ReadonlyDeep<ConfigType<typeof config>>;
Making ConfigType infer readonly config type by default. I think it is the best solution, however it introduces a BREAKING CHANGE
Sounds great! Would you like to create a PR for this issue? We can wait with merging it till the next major release (due to a breaking change)
Sounds great! Would you like to create a PR for this issue? We can wait with merging it till the next major release (due to a breaking change)
Sure!