Home > Software design >  How can I create typings to describe this common library pattern (callable namespace)?
How can I create typings to describe this common library pattern (callable namespace)?

Time:10-28

I'm trying to write TypeScript typings for an existing Javascript library. The library is packaged as CJS, and exports several variables and functions at the top level. It also exports a single top level "namespace like" object with the same variables and functions. (I know this is not a "recommended" pattern; I'm not here to discuss that.)

All of these are valid in an ESM (.mjs) context:

// Default export
import lib from "lib";
const x = lib(...);
const y = lib.b(...);

// Top level function, namespace-like variable
import {b, lib} from "lib";
const x = lib(...);
const y = b(...);

I want to write typings that describe this pattern. I can write top-level exports like

export function b(...);
declare function lib(...);
export default lib;

but I don't see an easy way to create a namespace that re-uses the various top level exports, so I can't really merge the function declaration with a namespace declaration.

Likewise, I can wrap the other functions in a namespace to start with

export namespace lib {
  function b(...);
}

but I don't see how to re-export lib.b at the top level, as simply b.

I've tried putting the top level exports in their own file ("top-exports.d.ts"), which lets me write

export * from "./top-exports"; 
import * as lib from "./top-exports";
export { lib }

but then I can't make lib callable. I know that you can use "export assignment" to make a callable default (export = lib), but then I'm back to the problem of making a callable, namespace-like object with both top-level and namespace-wrapped exports in the same place.

CodePudding user response:

I have a solution that sort of works. export = lib is required if you want a a function merged with a namespace. The problem is that you cannot export * from a module that was created using export assignment (export =), so you have to re-export each item by hand:

// top-exports.d.ts
declare namespace lib {
  function a(...);
  type SomeType = ...;
  // etc
}
declare function lib(...);

export = lib;



// lib.d.ts
import * as lib from "./top-exports";
import {a, b, c, d, SomeType, OtherType} from "./top-exports";

export default lib;
export {lib, a, b, c, d};
export type {SomeType, OtherType};

Of course this is much harder to maintain, spreading definitions across two files, and having to remember to include any new stuff in two extra places (the import {...} and export {...} statements), but at least you don't have to declare anything twice. I'd still really love to see a better answer.

CodePudding user response:

Unless I misunderstand a requirement, I think this is what you're looking for:

lib.d.ts

type A = () => string;
type B = () => number;

declare const a: A;
declare const b: B;
// ...etc.

interface Lib {
  (): void;
  a: A;
  b: B;
  // ...etc.
}

declare const lib: Lib;

declare namespace lib {
  export type Num = number;
}

export {
  lib as default,
  a,
  b,
  lib,
  // ...etc.
};

module.ts

import {a, lib} from './lib';

const x = a();
const y = lib();
const z = lib.b();
const n: lib.Num = 42;

  • Related