Search code examples
typescripttypescript-declarationstypescript-namespace

How can I merge typescript interfaces over multiple files in the same namespace


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.


Solution

  • 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 exported (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.

    Alternative: export config in a module

    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.