Note, this blog was written for TypeScript 1 and is frankly quite outdated. TypeScript 2 has introduced some major improvements, like providing @types/package packages, that can be set as peer or dev dependencies, avoiding many of the problems described below. So the most important take away when using TypeScript 2: enable "declarations" in the compiler options and set the "typings" field in package.json

I've been using TypeScript for over more than a year now on a full-time basis. And whenever people asked what I think of TypeScript, my answer always was “TypeScript is awesome and a real life saver. But the module system sucks big time.” Using a an arbitrary npm package in a strongly-typed fashion is quite a hassle. So let alone publishing one. But with the introduction of TypeScript 1.6 this has finally changed. TypeScript now follows the npm rules for module resolution. Sadly, so far this goodness has barely been documented although there are a lot of questions around the subject. In this blog post I’ll try address most questions.

In this blog post I will introduce two imaginary packages, ‘petstore’ and ‘pets’. The pets package just exposes a ‘Cat’ class that can ‘meow()’. The fun thing with TypeScript 1.6 is that if you set up your package properly, you will be able to just..

npm install pets --save

..in the petstore package folder and then consume the package directly from its TypeScript sources:

A plain ES6 style import is enough for a strongly-typed Cat class:

import {Cat} from “pets”

No further triple slash references, .d.ts files or tsd installs are required. Sounds promising right? But first, let’ s dive into the horrible pre-1.6 situation to make this blog a real emotional roller-coaster.

TypeScript npm packages prior 1.6

Feel free to skip this section to jump to the solution. It’s like the old- and new testament; you just will get a deeper understanding of the Good News if you study the old covenant.

Publishing strongly-typed packages before 1.6 was cumbersome because you had to distribute the typings of your package separately from the actual, compiled, package. You might wonder, why don’t you just ship the TypeScript source files with the packages then? Well, that introduces quite some problems:

You get ugly imports, like import {Cat} from “../node_modules/pets/src/cat.ts”. It bypasses the ‘main’ entry point of npm packages. It blows up most TypeScript tools as you will need refer to files outside the project sources. The package must be compatible with your compiler version. It is slow to compile additional packages as part of your build process.

In other words: don’t publish TypeScript source files as npm package. So what was the alternative? Answer: Ambient module declarations. Ambient module declarations allow you to ship the typings of a package separately from the package itself. The idea is pretty straightforward: An ambient module declaration specifies the type information of some global variable by name. An ambient module declaration looks like:

declare module "pets" {

export class Cat {

meow();

}

}

After that, you refer to this typings file from the consuming project using so called triple slash references:

/// <reference path="typings/pets/pets.d.ts" />

import {Cat} from "pets";

TypeScript can now determine the type of ‘Cat’ correctly because there is an import statement and an ambient module declaration which use exactly the same name; “pets” (including the double quotes!). That might seem quite OK at first sight, but those ambient declarations cause a lot of trouble:

You will have to write and supply ambient module declarations manually! The compiler has no built-in option to emit those declarations. As you can imagine, in no-time these declarations will be out of sync with the real sources. There are some community packages that can generate those files though but even those get you only halfway usually. You have to ship ambient declarations separately from your package. If you ship them with your package and require them with a triple slash reference like path=”./node_modules/pets/index.d.ts” the build of your package consumers will break as soon as the pets module exists twice in your module tree. Ambient declarations and package versioning is a big issue; publicly available typings are often behind. And even if they aren’t, your local copies still might be behind. Or your downloaded typings do not (exactly) match the actual version of the package you are using, which leads to confusing bugs. A fourth problem is that your module might require a module with an ambient module declaration which is also required by another module. For example both the express and cats package might require (typings for) the lodash package. Because these declarations live in a global namespace and are matched by name, they collide in no-time with each other. You know that you did ran into this issue as soon as you encounter an error like this one (…and probably a couple of hundred like these at the same time):

lib.core.d.ts(83,5): error TS2300: Duplicate identifier 'configurable'

To work around these issues the community has established some conventions and some tools (especially tsd) to make stuff a more bearable. The definitely typed repository is nowadays the primary source for ambient module declarations. So just remember; ambient module declarations are the root of all evil.