I am trying to refactor an unwieldy config interface/object by separating its various sections into separate files under a namespace
I've cleverly named Config
.
The documentation talks about namespaces that span multiple files and declaration merging of interfaces, but I can't seem to get them to work together.
src/config/index.ts
/// <reference path="./server.ts" />
import fs from 'fs';
import json5 from 'json5';
const _config = readConfig();
namespace Config {
export const config = _config;
export interface IConfig {
someGeneralProperty: {
// ...
}
}
}
function readConfig(): Config.IConfig {
return json5.parse(fs.readFileSync('./path/to/config.json', 'utf-8'));
}
function doSomeOtherStuff() {
// fails: Property 'server' does not exist on type 'IConfig'.
console.log(_config.server.host);
}
src/config/server.ts
/// <reference path="./index.ts" />
namespace Config {
export interface IConfig {
server: {
host: string;
port: number;
}
}
}
src/index.ts
// fails: Module '"./config"' has no exported member 'config'.
import { config } from './config';
// fails: Cannot use namespace 'Config' as a value.
// fails: Namespace 'Config' has no exported member 'config'.
import config = Config.config;
I've tried several variations of exporting things, such as export default Config;
, export namespace Config {...}
in each of the src/config/...
files, changing export const config
to export var config
. In src/config/index.ts
I tried export * from './server'
. Nothing seems to help.
I have a feeling I'm just going about this all wrong.
Oddly, the interfaces within the namespace in every file are exported from the namespace, so in src/index.ts
, I can do:
import IConfig = Config.IConfig;
let c: IConfig;
console.log(c.server.host);
but I cannot do that in either src/config/index.ts
nor src/config/server.ts
.
At first you should decide yourself, if you want to assign the config
object to a module scope (i.e. import
/export
) or in the global scope (i.e.window
in browsers, global
in node).
The main purpose of namespaces is to define properties/values on the global scope. As you pointed out correctly with the links, equally named namespaces are merged - that includes contained inner members like the IConfig
interface.
Here is the deal: Merging only happens when the file containing the namespace
is a script (a non-module file without import
/export
at top-level).
In src/config/index.ts
, you've got import
statements, so the file becomes a module and namespace Config
does not get merged. Instead it is rather a module internal namespace, which is not even export
ed (see Needless Namespacing, Do not use namespaces in modules in the docs). The Config
namespace in src/config/server.ts
forms its own global namespace (non-module file), that is why you can still use the contained IConfig
type.
In summary, if you want to have the config (value and type) globally, make sure, every part of the multi file part namespace is declare in a non-module file. If the config is to be exported from a module (preferred way if feasible!; better encapsulation, no global scope pollution, the "modern" way), read on.
src/config/server.ts:
export interface ServerConfig {
server: {
host: string;
port: number;
}
}
// you could also read a server-specific config value here, export it
// and merge it with a separately read common config value in index.ts
// export serverConfig: ServerConfig = readServerConfig()
src/config/index.ts:
import { ServerConfig } from "./server"
interface CommonConfig {
someGeneralProperty: {
// ...
}
}
export type IConfig = CommonConfig & ServerConfig
export const config: IConfig = readConfig(); // whatever readConfig looks like
src/index.ts:
import { config } from './config';
config.server;
config.someGeneralProperty
Feel free to adjust the parts, you need.