4 Terrible Ways (and 4 Better Ways) to pacify TypeScript

TS7016: Could not find declaration file

tsc, all the freaking time

My favorite thing about TypeScript is gradual typing. I don’t have to prove myself to the compiler because it lets me opt-out. But sometimes opting out is harder than other times.

When I import a JS module that is not built for TS, the compiler gets in my way. It says stuff like:

index.ts:2:24 - error TS7016: Could not find a declaration file for module 'deppy'. '/home/jessitron/code/atomist-blogs/deppy/index.js' implicitly has an 'any' type.
  Try `npm install @types/deppy` if it exists or add a new declaration (.d.ts) file containing `declare module 'deppy';`

2 import * as deppy from "deppy"
                         ~~~~~~~

How can I get past this error?

Here are eight fixes, from worst to best:

Terrible Fix #1: Don’t bother

TypeScript will turn your code into JavaScript even when there are compile errors.

You can go ahead and run your program without fixing the error. If the first thing you wanted to do with that library was try something, go ahead and try it!

Caveat: the compiler option --noEmitOnError makes TypeScript refuse to emit JavaScript when it finds compile errors. Run tsc --noEmitOnError false to get around it.

Caveat: Tests might block on the error. This happens to me because my test uses mocha and we run mocha with on-the-fly compilation of TypeScript (our npm test script runs mocha --require espower-typescript/guess "test/**/*.test.ts"). Sometimes mocha crashes on this error, and outputs nothing, and doesn’t return failure, so my build looks successful. 😡 To get around it: compile the test (letting it fail), then run mocha on the JavaScript output, like: $(npm bin)/mocha myParticularTest.test.js

Terrible Fix #2: Allow implicit any

In tsconfig.json, in the compiler options, set "noImplicitAny": false. This will make it shut up.

But then you won’t get an error when you forget to declare the type of a function parameter. This is bad!

Caution: accidentally writing code like function decideAThing(importantParameter) {...} will assign the type any to importantParameter. Then you get no type checking on it, and it won’t be included in refactorings on its type. This has burned us before. 🔥

Terrible Fix #3: Allow all JS module imports

This is actually my favorite, but my coworkers do not approve.

Make a file anywhere among your .ts files. Put in it:

declare module "*";

That’s it, party time! All modules not otherwise typed are explicitly declared as any.

Some people like to call this file types.d.ts.

Sometimes I like to call it allowJavaScriptModules.ts. That describes its purpose. It ain’t like this file contains useful types.

You can make a .d.ts file directly, or you can make a .ts file and let the compiler output .d.ts for you.

Watch out: if you create a .d.ts file, then you must teach .gitignore to care about it. You probably have compiler output (including .d.ts) excluded from git. Add a line to the end of .gitignore with a “!” for “don’t ignore this,” followed by your filename:

!types.d.ts

Kind-of-terrible Fix #4: Allow this one JS module import

Make a file somewhere among your TypeScript source. Call it types.d.ts (or whatever; see above) and put in it:

declare module "your-package-of-interest";

except put the name of the module you want to import in the string.

This makes an explicit declaration of that module as type any. This is called an “ambient declaration” because it’s floating around in global space among the source files the compiler reads.

Watch out: see the note in Fix #3 about .gitignore, or this will work for you and no one else. If that types.d.ts file is ignored by git, it’ll stay on your filesystem, not make it into the repository.

Decent Fix #5: Declare only the types you need

If you want types, but no one else has created them, then you create what you need. This is not easy, but it gets you the type safety you crave.

Start with a .d.ts file anywhere in your source directories. (See Fix #3). You can put all of these in types.d.ts or each in its own file, like types/your-package-of-interest.d.ts. Declare the module like this:

declare module "your-package-of-interest" {
    type Stuff = string;
    function happyFunction(parameters: stuff[]): Stuff;
    function happyFunction(orThis: number): Stuff; // overloaded function decl
    // etc etc, all the exports you choose to include. 
}

If the module exports just one thing (like a function or a class), then add a line at the bottom to specify the one exported thing:

declare module "happy-function" {
    function happyFunction(parameters: stuff[]): string;
    exports = happyFunction;
}

Remark: it took me a while to figure this out. I tried to use compiler options like --types and --typeRoots but those didn’t do anything. It’s easier than that. Just remember to put the module name in quotes; otherwise it won’t find the declaration.

Hero Fix #6: Contribute type declarations to @types

While you’re making types for the package, why not make them complete? Then you can submit a PR to DefinitelyTyped, and everyone can have use of them!

Best Fix #7: Get the type declarations from @types

The ideal solution is right in the TS7016 error message: npm install @types/your-package-of-interest.

This pulls from a special NPM username, @types, where people publish TypeScript declarations separately from the packages they declare.

Consideration: When you bring in the type declarations, you can do this two ways. The default npm install will put them in your project’s dependencies. Not perfect; these types are used at compile time, not runtime. Normally, one brings in build-time dependencies with npm install --save-dev, and they get recorded in devDependencies. These don’t ship with your production code, or add to transitive dependencies.

Caution: if your project is a library, then people who use your library will need those types. If you put them in devDependencies, your downstream users won’t get them, so they’ll get compile errors. When you make a library, put those types in dependencies (the default) instead. Do not use --save-dev to install packages from @types when you publish a TypeScript library.

Great Fix #8: Upgrade the library

Sometimes people add TypeScript type definitions to their package. This happened to me with boxen. I made my own types (fix #5), and then they released a new version and poof! All the types were there. I deleted my own declarations. Before you do anything harder, try the latest version of the package.

Conclusions

Eight! That’s a lot of ways to solve a single problem.

This is the flexibility of TypeScript’s gradual typing. You can start with crude workarounds, and hope someone else solves the hard problems (by publishing the types). Or you can contribute yourself! Or you can live without types, for one module or all. All these options are open to you. Choose how much the compiler challenges you.

And remember: always read the error message.

originally published on the Atomist blog