1
votes

I'm trying to add types for the config module specific to our app. The config module is dynamically generated from a JSON file so it's tricky to type. Since it's a node module, I'm using an ambient module for the typings.

// config.d.ts
declare module 'config' {
  interface AppConfig {
    name: string;
    app_specific_thing: string;
  }
  const config: AppConfig;
  export = config;
}

How do I also export AppConfig so I can use it as a type like so:

import * as config from 'config';

const appConfig: config.AppConfig;

Attempts

  • If I export AppConfig directly in the config module it errors with:

    TS2309: An export assignment cannot be used in a module with other exported elements.

  • If I move AppConfig to another file (e.g. ./app_config) to hold the exports and import them into config.d.ts it errors with:

    TS2439: Import or export declaration in an ambient module declaration cannot reference module through relative module name.

  • If I put the AppConfig export in the same file, but outside the config module, it errors with:

    TS2665: Invalid module name in augmentation. Module 'config' resolves to an untyped module at $PROJ/config/lib/config.js, which cannot be augmented.

This is similar to Typescript error "An export assignment cannot be used in a module with other exported elements." while extending typescript definitions with the requirement that I want to be able to import AppConfig as a type directly in other TS files.

1
do you really need to import it by the name AppConfig? I tried importing * as config and using type AppConfig = typeof config, which worked, so that's a workaround at leastmichaeln

1 Answers

6
votes

The answer requires a confusing Typescript concept:

Declaration merging - the compiler merges two separate declarations declared with the same name into a single definition. In this case, we create two declarations of config.

// config.d.ts
declare module 'config' {

  // This nested namespace 'config' will merge with the enclosing 
  // declared namespace 'config'.
  // https://www.typescriptlang.org/docs/handbook/declaration-merging.html
  namespace config {
    interface AppConfig {
      name: string;
      app_specific_thing: string;
      my_enum: FakeEnum;
    }

    interface MyInterface {}

    // See side note below
    type FakeEnum = 'A' | 'B' | 'C';
  }

  const config: AppConfig;
  export = config;
}

You can use the imports like so:

import * as config from 'config';
import { FakeEnum, MyInterface } from 'config';

As a side note, you cannot use enums with an ambient module (the declare module 'config') because enums compile to a JS object and you can't add new object to a module you don't control. You can work around the issue by faking an enum with a union type:

type FakeEnum = 'A' | 'B' | 'C';