How to augment a JS module to add a new interface?

Viewed 181

I have a TypeScript NodeJS project with allowJS, and one of my CommonJS JavaScript files contains a class Logger. I would like to augment the JS file with an interface declaration ILogger, so that I can import {Logger, ILogger} from './logger'; in TypeScript files.

According to the answer to Using interface defined in JSDoc in Typescript, TypeScript does not support JSDoc @interface, so I should be using a declaration file .d.ts instead. But I can’t figure out how to combine a JS file containing a class with a declaration file containing an interface so that both of them can be imported from a typescript file.

Attempt 1: Both logger.js and logger.d.js: TypeScript only reads types from logger.d.ts

Putting the class in a js file and the interface in a d.ts file with the same name excluding extension does not work, because typescript only reads the types from the d.ts file and ignores the class in the JS file:

Error: app.ts:1:18 - error TS2724: '"./logger"' has no exported member named 'Logger'. Did you mean 'ILogger'?

// logger.js
//@ts-check
exports.Logger = class Logger {
  /**
   * @param {string} msg 
   * @returns {void}
   */
  info(msg) {
    console.log(msg);
  }
}

// logger.d.ts
export interface ILogger {
  info(msg: string): void
}

// app.js
import {ILogger, Logger} from './logger';

export function f(logger: ILogger) {
  logger.info('f');
}

export function main() {
  const logger = new Logger();
  f(logger);
}

// tsconfig.json
{
  "extends": "@tsconfig/recommended/tsconfig.json",
  "compilerOptions": {
    "allowJs": true
  }
}

// package.json
{
  "devDependencies": {
    "@tsconfig/recommended": "^1.0.1",
    "typescript": "^4.4.3"
  }
}

Attempt 2: Augmenting module in a .d.ts file referenced by <reference path=…/>: TypeScript only reads types from logger.js

When I try module augmentation (declare module '…') of the ./logger JS module with a declaration file referenced in a triple-slash directive (<reference path="…"/>), the augmentation does not seem to work and TypeScript only reads the class from the JS file and ignores the the declare module:

app.ts:2:9 - error TS2724: '"./logger"' has no exported member named 'ILogger'. Did you mean 'Logger'?

TypeScript gives no error for a global declaration that I added, so I know that it is reading the types.d.ts file.

// logger.js, tsconfig.json, package.json: same as in Attempt 1

// types.d.ts:
declare module './logger' {
  export interface ILogger {
    info(msg: string): void
  }
}
declare function myGlobalFunc(): void

// app.ts:
/// <reference path="./types.d.ts"/>
import {ILogger, Logger} from './logger';

export function f(logger: ILogger) {
  logger.info('f');
}

export function main() {
  const logger = new Logger();
  myGlobalFunc();
  f(logger);
}

Attempt 3: Augmenting logger.js module in app.ts does work but is not cleanly reusable

When I try module augmentation (declare module '…') of the ./logger JS module with a declaration in a TypeScript module, it does work without any TypeScript errors. I guess the only problem is that I have to duplicate this module extension or import the TypeScrpt module that does the extension each time I want to use the extension.

// logger.js, tsconfig.json, package.json: same as in Attempt 1

// app.js
import {ILogger, Logger} from './logger';

declare module './logger' {
  export interface ILogger {
    info(msg: string): void
  }
}

export function f(logger: ILogger) {
  logger.info('f');
}

export function main() {
  const logger = new Logger();
  f(logger);
}

Is there a better way to add a TypeScript interface declaration to a JavaScript CommonJS module?

0 Answers
Related