How to use node-config in typescript?
Asked Answered
K

9

29

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'.
Kneedeep answered 12/6, 2018 at 9:21 Comment(0)
K
16

config.get utility can be used to get the config values like so:

import config from 'config';

const port: number = config.get('server.port');
Kneedeep answered 12/6, 2018 at 9:50 Comment(5)
It is just a workaround. still looking for better answer.Kneedeep
This is how config is expected to be used. get and has are actual reasons to use it. It provides too much magic to be properly typed. You could extend its type like const config: IConfig & MySchema = require('config') or with custom d.ts typing, but you cannot be sure that expected properties are there because they can exist in one config but not in another.Inelegancy
Not sure what a "better answer" would look like, but it's important to note that config.get() supports generics. You could do config.get<number>('server.port') and that might be a little cleaner in some instances.Diane
automatic typing could be certainly implemented in the library using ts template litteralsMilly
I have many reasons to use JSON format for the config file, but I encountered several issues after switching my NodeJS project to typescript; the trick explained by @Diane fixed some, thanksMeal
S
19

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:

  • We can use the rich and battle-tested features available from node-config without needing to re-invent the wheel
  • We have a strongly-typed, well documented config map which can be imported and used from anywhere inside our TS code
Swoosh answered 20/4, 2020 at 20:40 Comment(0)
K
16

config.get utility can be used to get the config values like so:

import config from 'config';

const port: number = config.get('server.port');
Kneedeep answered 12/6, 2018 at 9:50 Comment(5)
It is just a workaround. still looking for better answer.Kneedeep
This is how config is expected to be used. get and has are actual reasons to use it. It provides too much magic to be properly typed. You could extend its type like const config: IConfig & MySchema = require('config') or with custom d.ts typing, but you cannot be sure that expected properties are there because they can exist in one config but not in another.Inelegancy
Not sure what a "better answer" would look like, but it's important to note that config.get() supports generics. You could do config.get<number>('server.port') and that might be a little cleaner in some instances.Diane
automatic typing could be certainly implemented in the library using ts template litteralsMilly
I have many reasons to use JSON format for the config file, but I encountered several issues after switching my NodeJS project to typescript; the trick explained by @Diane fixed some, thanksMeal
G
12

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

Gastrotrich answered 18/3, 2019 at 11:44 Comment(0)
D
5

From 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'));
Dying answered 14/2, 2019 at 1:7 Comment(0)
M
3

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:

enter image description here

Manana answered 1/12, 2021 at 20:35 Comment(0)
O
1

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.

Othaothe answered 31/1, 2023 at 13:48 Comment(0)
F
0

Alternative 1

Use node-config-ts

Alternative 2

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.

Fructificative answered 6/5, 2022 at 12:36 Comment(0)
O
0

node-config does not detect your config types. IConfig is simply the description of the config library interface.

You can tell typescript what type you are expecting by using the "get" method with a type argument like this:

const server = config.get<string>("server");

NOTE: config.get throws an error if your config value is missing. This is intentional to help catch bugs.

If you prefer runtime safety, check for the existence of a config value with the "has" method.

config.has("server")

config.has("api.url") for nested parameters

However, if your config value turns out to be a different type, such as an object or number instead of a string, you could run into other runtime problems.

Validate with a pattern something like this:

const maybeServer = config.has("server") ? config.get("server") : null;

const server = typeof maybeServer === "string" ? maybeServer : "default";

After validating in this way, typescript will know your variable's type.

That's the "simple" answer.

For a more comprehensive, enterprise-level solution, use a schema validator like Zod.

Oviduct answered 15/3, 2023 at 20:0 Comment(0)
R
-5

You can use the any return type.

const serverConfig: any = config.get('server');
Remunerative answered 17/4, 2021 at 18:35 Comment(1)
encrypted-tbn0.gstatic.com/…Horizontal

© 2022 - 2024 — McMap. All rights reserved.