How to write multi-module Typescript declaration files

Thomas James Byers
3 min readOct 16, 2016

--

Are you using Typescript and Lodash together? If so you’re probably not as type safe as you think.

Once you install the Lodash typings from @types/lodash (or from DefinitelyTyped) the “_” type becomes globally available within your project. If you start using Lodash without having imported it first, not only will Typescript fail to warn you, it will actually provide intellisense for methods you don’t even have!

This problem isn’t unique to Lodash. The same is true of core-js and many other multi-module packages.

Wait, what’s a multi-module package?

Let’s say you want to use the map method from lodash. You have three options for importing it:

import * as _ from ‘lodash’
import { map } from 'lodash'
import * as map from 'lodash/map'

The first line will make all lodash methods available, but the second and third only import the map method. The difference between these two is relevant when using a tool like Webpack.

When you import a module, Webpack will bundle all of it’s exports even if you only use some of them. Things will change with the introduction of tree shaking in Webpack 2, but in the meantime importing from ‘lodash/map’ is the only way to avoid ending up with the whole of Lodash in your bundle.

So what’s the problem?

To make sure Typescript knows about ‘lodash/map’ the Lodash declaration file includes the following snippet:

declare module “lodash/map” {
const map: typeof _.map;
export = map;
}

The declare keyword adds “lodash/map” to the global namespace so all files know they can import it. Once imported, the map type is made available via “export =”. So far, so good.

But what is this _.map on the second line? It turns out the Lodash declaration file also includes this…

declare var _: _.LoDashStatic;

LoDashStatic contains the whole of Lodash. The declare keyword here is polluting the global namespace with every method in Lodash.

Why did the author do this? To be DRY. You don’t want to duplicate the same type definition - once to export on its own, and once to export as a method on “_”. The only way to have two module declarations share the same type is to declare it at the top-level.

The Workaround

We want the module declarations to be global, but not the variable declarations. If a declaration file contains a top-level export, it’s declarations aren’t added to the global namespace.

We could declare the variable in a separate file and then import it, but this isn’t as easy as it seems. A top-level import will have the same side-effect as a top-level export, rendering all our module declarations useless.

However, the import doesn’t have to be at the top level. Instead we can do something like this:

declare module “lodash” {
import * as _ from 'lodash-static'
export = _;
}
declare module “lodash/map” {
import * as _ from 'lodash-static'
const map: typeof _.map;
export = map;
}

Now lodash static is in a separate file where it doesn’t pollute the global namespace, but we can still import it for re-export as part of a module.

--

--