TS-type aware configuration files
The problemโ
๐ก I feel frustrated any time, when i have no autocomplete on typing a property in a config file for a popular tool
like prettier.config.json
...
Sometimes, the IDE/CodeEditor knowns "schema" of json/yaml
config and hints when you type(for ex. tsconfig.json
, package.json
).
But frequently it is not so.
Notice the problem is not about productivity or business-related code quality metrics
but what developer experience should be i expect in 2022+ year
If you use the WebStorm and your IDE helps you cook all config files in your project, you are lucky and may not read the below part of this blog ๐. But even in this case, I will suggest some advantages you may like. See the Conclusion section.
TypeScript is everywhereโ
Almost all popular tools in the nodejs world follow trends:
- involve TypeScript for public API and/or internal development(even JS-written tools try to provide
.d.ts
files as contract) - allow different file formats for its configuration files (json,json5,ini,yaml,js)
Both facts allow us to use the following approach...
TL;DR;โ
- Use whenever possible config files in
js
format instead ofjson,yaml,ini
- Enable TS checking and annotate config object with TS-types using
jsdoc
The code snippet:
/**
* .{my-fancy-tool-config}.js
*/
// @ts-check
/** @type {import('my-fancy-tool/path-to-types').ConfigType} */
const config = {
thisPropertyAutocompleted: ```
and marks as error in case of
not existing/missing key
or wrong type of value```,
}
module.exports = config
Examplesโ
.eslintrc.jsโ
// @ts-check
/**
* @type { import('eslint').Linter.Config }
*/
const config = {...}
module.exports = config
webpack.config.jsโ
// @ts-check
/**
* @typedef { import('webpack').Configuration & {
* devServer: import('webpack-dev-server').Configuration
* }} WebpackConfiguration
*
* @type { WebpackConfiguration }
*/
const config = {...}
module.exports = config
prettier.config.jsโ
// @ts-check
/**
* @typedef { import('prettier').Options } PrettierConfig
* @typedef { {files: string | string[], options: PrettierConfig} } PrettierOverrideConfig
* @type { PrettierConfig & { overrides?: PrettierOverrideConfig[] } }
*/
const config = {
...,
tabWidth: 4,
// "overrides" not provided by original typings
// so we add our custom "overrides" (see jsdoc comment above)
overrides: [
{
files: '*.{json,yaml,yml}',
options: { tabWidth: 2 },
},
],
}
module.exports = config
stylelint.config.jsโ
// @ts-check
/**
* @type { import('stylelint').Config }
*/
const config = {...}
module.exports = config
.testcaferc.jsโ
Below typings is a most hacky example of what did I have to do in the real world, but the solution seems ok to me.
// @ts-check
/// <reference path='node_modules/testcafe/ts-defs/index.d.ts'/>
/**
* @typedef { Partial<TestCafeConfigurationOptions> & {
* hooks?: {
* runTest?: {
* before?(): void
* after?(): void
* }
* }
* } } TestCafeImprovedConfig
*/
/**
* @type { TestCafeImprovedConfig }
*/
const config = {}
module.exports = config
docusaurus.config.jsโ
// @ts-check
/** @type {import('@docusaurus/types').DocusaurusConfig} */
const config = {...}
module.exports = config