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 withoutimport
/export
at top-level).In
src/config/index.ts
, you've gotimport
statements, so the file becomes a module andnamespace Config
does not get merged. Instead it is rather a module internal namespace, which is not evenexport
ed (see Needless Namespacing, Do not use namespaces in modules in the docs). TheConfig
namespace insrc/config/server.ts
forms its own global namespace (non-module file), that is why you can still use the containedIConfig
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.
Alternative: export config in a module
src/config/server.ts:
src/config/index.ts:
src/index.ts:
Feel free to adjust the parts, you need.