Fixing TypeScript declarations

Posted on July 26, 2019

Broken declarations

When using TypeScript with npm modules you can usually install an additional package containing type declaration which will allow TypeScript to more correctly check that your code is correct. For example if you are working with react you would install @types/react.

Sometimes a type declaration package will be out of sync with the latest version of an npm package. I was recently working on a hobby project and was using webpack-dev-server with @types/webpack-dev-server. I was trying to use the documented devServer.mimeTypes property but unfortunately TypeScript was marking it as an error since the latest version of @types/webpack-dev-server hadn’t included that property.

I’ve encountered and fixed similar problems before but it’s always full of trial-and-error so I almost feel like I’m starting from scratch every time. I was trying a bunch of different approaches this time but nothing worked as expected. In the end I was helped by Karol Majewski in the #typescript channel of the Functional Programming Slack

The solution

The main feature we need is declaration merging. The concept is quite simple. If you declare something like an interface in more than two places, the interfaces get merged. The difficult part comes from figuring out how to “access” the scope of an interface you didn’t define yourself. If you define the interface in the wrong scope you will end up defining a new interface and you will not get a merged interface.

My original incorrect attempt was looking at the source of @types/webpack-dev-server/index.d.ts and trying to define the Configuration in the same way.

The error I made here was trying to replicate the definition. The right way to think of it is working with the shaped of the resulting exported module. You also have to refer to the package by it’s npm name. In this case webpack-dev-server has export = WebpackDevServer; which refers to both declare class WebpackDevServer and declare namespace WebpackDevServer from above. This means the correct declaration is as below:

There is one more complication. For unknown reasons the way declare namespace works is different based on whether you have any export/import statements in your file. Without imports/exports, your declaration will override the previous declaration and with imports/exports, your declaration will get merged. It doesn’t matter what you import or export. For example import * as http from 'http' works fine even though our declaration for webpack-dev-server has nothing to do with http. I did some Googling, because I was curious about the reason but couldn’t find why it works this way yet. I will update this post in case I figure out the reason.

In the end it turned out I didn’t need the mimeTypes property at all, but at least I figured out how to augment a type declaration. See the final version below.