After installing node-config
and @types/config
:
yarn add config
yarn add --dev @types/config
And adding config as described in lorenwest/node-config :
// default.ts
export default {
server: {
port: 4000,
},
logLevel: 'error',
};
When I am trying to use in my app:
import config from 'config';
console.log(config.server);
I am getting the error:
src/app.ts(19,53): error TS2339: Property 'server' does not exist on type 'IConfig'.
config.get
utility can be used to get the config values like so:
import config from 'config';
const port: number = config.get('server.port');
I'm taking a slightly different approach - defining the variables in JavaScript, and accessing them in TypeScript.
Using the following folder structure:
├── config
│ ├── custom-environment-variables.js
│ ├── default.js
│ ├── development.js
│ └── production.js
└── server
├── config.ts
└── main.ts
I define the configuration in the root config/
folder. For example:
// config/default.js
module.exports = {
cache: false,
port: undefined // Setting to undefined ensures the environment config must define it
};
// config/development.js
module.exports = {
port: '3000'
}
// config/production.js
module.exports = {
cache: true
}
// config/custom-environment-variables.js
module.exports = {
port: 'PORT'
}
Now, in TypeScript land, I define an interface to provide nicer autocomplete & documentation, and write some bridging code to pull in the config from node-config
into my config map:
// server/config.ts
import nodeConfig from 'config';
interface Config {
/** Whether assets should be cached or not. */
cache: boolean;
/** The port that the express server should bind to. */
port: string;
}
const config: Config = {
cache: nodeConfig.get<boolean>('cache'),
port: nodeConfig.get<string>('port')
};
export default config;
Finally, I can now import and use my config variables inside any TypeScript code.
// server/main.ts
import express from 'express';
import config from './config';
const { port } = config;
const app = express();
app.listen(port);
This approach has the following benefits:
node-config
without needing to re-invent the wheelFrom the previous, I was still having trouble where config
was not able to find the server
key from default.ts
.
Below is how I am using npm config module. Updated export default {
to export =
:
// default.ts
export = {
server: {
port: 4000,
},
logLevel: 'error',
};
Usage within the app [Same]:
import config from 'config';
console.log(config.get('server'));
Use this "import * as config from 'config';" instead of "import config from 'config';"
import * as config from 'config';
const port = config.get('server.port');
console.log('port', port);
// port 4000
config/development.json
{
"server": {
"port": 4000
}
}
and set NODE_ENV=development
export NODE_ENV=development
note: No need this NODE_ENV set if you use default
I use IConfig
interface, so I can set the config path first:
import { IConfig } from 'config';
export function dosomething() {
process.env["NODE_CONFIG_DIR"] = 'path to config dir';
//using get
const config: IConfig = require("config");
const port = config.get('server.port');
console.log('port', port);
//using custom schema
const config2: { server: { port: number } } = require("config");
console.log('config2.server.port', config2.server.port);
}
//port 4000
//config2.server.port 4000
您可以使用any
返回类型。
const serverConfig: any = config.get('server');
The only way I could make this work is by uninstalling @types/config
and modifying the type definitions to include my config files.
config.d.ts
declare module 'config' {
// Importing my config files
import dev from '#config/development.json'
import test from '#config/test.json'
import prod from '#config/production.json'
// Creating a union of my config
type Config = typeof dev | typeof test | typeof prod
var c: c.IConfig;
namespace c {
// see https://github.com/lorenwest/node-config/wiki/Using-Config-Utilities
interface IUtil {
// Extend an object (and any object it contains) with one or more objects (and objects contained in them).
extendDeep(mergeInto: any, mergeFrom: any, depth?: number): any;
// Return a deep copy of the specified object.
cloneDeep(copyFrom: any, depth?: number): any;
// Return true if two objects have equal contents.
equalsDeep(object1: any, object2: any, dept?: number): boolean;
// Returns an object containing all elements that differ between two objects.
diffDeep(object1: any, object2: any, depth?: number): any;
// Make a javascript object property immutable (assuring it cannot be changed from the current value).
makeImmutable(object: any, propertyName?: string, propertyValue?: string): any;
// Make an object property hidden so it doesn't appear when enumerating elements of the object.
makeHidden(object: any, propertyName: string, propertyValue?: string): any;
// Get the current value of a config environment variable
getEnv(varName: string): string;
// Return the config for the project based on directory param if not directory then return default one (config).
loadFileConfigs(configDir?: string): any;
// Return the sources for the configurations
getConfigSources(): IConfigSource[];
// Returns a new deep copy of the current config object, or any part of the config if provided.
toObject(config?: any): any;
/**
* This allows module developers to attach their configurations onto
* the 6 years agoInitial 0.4 checkin default configuration object so
* they can be configured by the consumers of the module.
*/
setModuleDefaults(moduleName:string, defaults:any): any;
}
interface IConfig {
// Changed the get method definition.
get<K extends keyof Config>(setting: K): Config[K];
has(setting: string): boolean;
util: IUtil;
}
interface IConfigSource {
name: string;
original?: string;
parsed: any;
}
}
export = c;
}
Then I can do something like this:
Use node-config-ts
node-config-ts only updates the types every time you npm i
, but alternative 2 is good if you want to be more explicit about which configuration files you are reading from, as they are imported directly in your project. This also means that tools such as nx knows to recompile your project if the config files change.
src/Config.ts
import config from 'config';
import DefaultConfig from '../config/default';
import CustomEnvironmentVariables from '../config/custom-environment-variables.json';
// If you have more configuration files (eg. production.ts), you might want to add them here.
export const Config = config.util.toObject() as typeof DefaultConfig & typeof CustomEnvironmentVariables;
src/app.ts
import {Config} from './Config';
// Has the correct type of "string"
console.log(Config.server);
tsconfig.json
{
// ...
"compilerOptions": {
// ...
// add these if you want to import .json configs
"esModuleInterop": true,
"resolveJsonModule": true,
},
}
If you are using a monorepo and try to import default.ts
in a child project, you might get the error...
error TS6059: File 'config/default.ts' is not under 'rootDir' 'my-project'. 'rootDir' is expected to contain all source files`.
...then you might have to implement this answer .
In my opinion the biggest drawback of type safety is that it can create a false sense of security due to the common confusion between compile-time safety and runtime safety. It is especially true for node-config where the config is the product of merging multiple files and environment variables. This is why any solution that applies a type to your config without checking that it actually maps that type at runtime can create problems down the line. To solve this you could have a look at type guarding solutions like typia or zod for example.
Personally I use tools that are usually already present in my projects: JTD + Ajv . Here is a recipe if somebody is interested.
config
├── config.jtd.json
├── custom-environment-variables.js
├── default.json
└── development.json
src
├── config.ts
types
└── index.ts
The file config/config.jtd.json
is written by hand, it looks like this (optionalProperties is used to ignore some things added by node-config, merging the interface with IConfig provided by node-config might be a better way to do the same thing):
{
"properties": {
"port": { "type": "uint32"}
},
"optionalProperties": { "util": {}, "get": {}, "has": {} }
}
The file types/index.ts
contains a Config
interface, it was created using jtd-codegen :
jtd-codegen config/config.jtd.json --typescript-out types
Then src/config.ts
performs the validation and type casting:
import fs from 'fs'
import config from 'config'
import Ajv from 'ajv/dist/jtd'
import { type Config } from '../types'
const validate = new Ajv().compile(JSON.parse(fs.readFileSync('config/config.jtd.json', 'utf8')))
if (!validate(config)) throw new Error('invalid config', { cause: validate.errors })
config.util.makeImmutable(config)
const typedConfig = config as unknown as Config
export default typedConfig
Now when importing src/config.ts
you have both compile-time type safety and runtime validation no matter what. The config object is also immutable so it will stay safe. Note that the config.get method is not covered, but I don't think it is necessary now that safety is ensured. You might even remove it.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.