TypeScript has multiple syntaxes for imports. When should you use which?

It depends. 😞

Most of the time, the module exports multiple things

There are two great ways to import from another module when the module exports an object with properties. This is the common case.

Import the whole module, giving it a name:

import * as child_process from "child_process";

// then later...
child_process.spawn(...);

or pick the names you want to import:

import { spawn } from "child_process";

// then later
spawn(...);

Sometimes the module exports just one thing

This doesn't work when the module doesn't export an object with properties. Some modules export a function or a class instead.

How can you know? Good question. You have to look at the module's code or look at examples.

Maybe it declares a default export for TypeScript

TypeScript has this concept of export default to declare the single thing that is exported. I wish it didn't, because it makes this even more complicated.

If a module declares a default export, then you must bring it in like this:

import thing from "thing";

Now you have a function or a class (whatever its default export is) in thing.

Or more likely, it overrides the exports object with a single thing

More commonly in JavaScript (CommonJS?) modules, a module author will override module.exports to a function or class instead of adding properties to the exports object like a polite module would.

Look at the JS examples to find out what it is. For example, my favorite npm module boxen has this example, showing that what you get from it is a function:

const boxen = require('boxen');
 
console.log(boxen('unicorn', {padding: 1}));

In this case, how to import it depends on your compiler options. Try this style:

import * as boxen from "boxen";

Now this is confusing, because boxen here is not an object. It's a function. You didn't get all the things, you got the one thing.

BUT if in your compiler options you set "esModuleInterop": true, then TS will get clever and you can write this as:

import boxen from "boxen";

You only get one or the other style through your whole project. With esModuleInterop, TS sets up the default imports for you. But then importing with * does not work. You get to pick.

Or! How about a third way? This one works in either case, although it is not as pretty:

import thing = require("thing");

How do you know??

How are you supposed to know whether a module exports one thing or multiple? Maybe docs, or else look at the code.

Or, try both the "import * as blah" and the "import blah" syntaxes and see which works.

Sometimes you can look at examples and see how they use the export. These can be hard to translate from JS. For instance, in the npm page for boxen it shows:

const boxen = require('boxen');
 
// then later
boxen('unicorn', {padding: 1});

This shows that the thing it gets from requiring the module is being used as a function. So, it's a single function. See above; syntax depends on your compiler options. Or use import boxen = require("boxen");.

In contrast, the npm page for chalk shows:

const chalk = require('chalk');
 
console.log(chalk.blue('Hello world!'));

Here, you can see that the thing it got from requiring boxen is being used as an object. Use import * as chalk from "chalk";.

How to know what a TypeScript module exports

A TypeScript module can say export default myFunction to export just one thing. Use import myFunction from "./myModule" to bring it in.

More commonly, TypeScript modules say export myFunction in which case myFunction will be one of the properties on the exported object. Use import { myFunction } from "./myModule" to bring it in.

Do I have to name it after the module?

No, there's nothing stopping you from naming your import whatever. You can import booger from "boxen" and then call booger("put this in a box"). This seems ridiculous. If this bothers you, tslint can yell at you for it: turn on the 'import-name' rule from tslint-microsoft-contrib.

When the module exports an object it is not so ridiculous. With lodash:

import * as _ from "lodash";

_.groupBy(...);

See, here the methods etc on the imported object all have the names assigned by the module author, so it doesn't matter if I name the module object itself something cute. Please export an object, please.

Am I importing a module or a package?

If the import starts with "." then it's a relative import, and it's a module. (A module is a file.) Otherwise, it's conceptually a package, but really it's the top-level module within the package. The top-level module is usually the package's index.js, but that can be overridden in the main element of the package'spackage.json file.

Sometimes 'import' means 'run'

You can import a script for side effects only:

import "./set_up_global_logging";

Fall back to JS

You can always const thing = require("Anything"); just like in JS, but you won't get typing. You also won't get compile-time checking that the module is available.

This will load the module dynamically, so you can conditionally load a module and then use it. This is handy when the module takes a long time to load, for instance.

How do you really feel, Jess?

Can we all just export an object, please? That way the properties have the name we give them instead of whatever name people assign them. I don't like default exports. But some people do, and if I want to use their packages then I'll deal with it. But I'm sad; I wish I could use import * as module from "module" all the time.

Appendix: Troubleshooting

Here are some error message translations.

No type declarations

Any of these imports can result in a compile error: error TS7016: Could not find a declaration file for module 'whatever-module'.

Here are your choices:

  • Ideal: try npm install --save-dev @types/whatever-module just in case someone has written type declarations for it. If this works, great.
  • Do this one: create a file called types.d.ts at the root of your source directory containing declare module "whatever-module"; . This counts as an explicit "any" declaration for the specific module.
  • Cheating: set "noImplicitAny": false in your compiler options in tsconfig.json. This makes TypeScript assume an "any" type for all modules. It also makes the compiler ignore when you forget to specify a type for each function parameter, so I don't recommend it.
  • Less cheating: create a file called types.d.ts at the root of your source directory containing declare module "*"; . This counts as an explicit "any" declaration for every module.
  • Hard-core: create type declarations for the module you want to use, specific enough for the ways you use it. There's a whole guide for this. It's hard.

No default export

error TS1192: Module '"thing"' has no default export.

This means you tried to use import thing from "thing" but TS didn't find a default export and you didn't ask it to simulate them.

Here are your choices:

  • Use import * as thing from "thing"
  • Use import thing = require("thing");
  • Turn on esModuleInterop in tsconfig.json (caution: this might make other, existing imports behave differently)

Default is not populated

Here's a runtime error: Uncaught TypeError: thing.default is not a function

In this case I'm using thing as a function, because I expected the module to export a function. But this is not from a TypeScript module, so it doesn't use export default, nor from a module that tries to support TS, which would politely define exports.default.

One possible cause of this is: you used import thing from "thing" and it compiles because allowSyntheticDefaultImports is true in tsconfig.json. That option affects compilation only, and doesn't create magic defaultiness in the emitted JS.

Either turn off allowSyntheticDefaultImports or turn on esModuleInterop. The latter does impact emitted JS.

A namespace-style import cannot be called or constructed

Here's a compile error: error TS2349: Cannot invoke an expression whose type lacks a call signature. Type 'typeof internal' has no compatible call signatures. Later it says: A namespace-style import cannot be called or constructed, and will cause a failure at runtime.

This one happens when I have import * as thing from "thing"; , I've turned esModuleInterop on in tsconfig.json, and the imported module exports one thing that is not an object.

Choices:

  • use import thing from "thing"
  • use import thing = require("thing")
  • Turn off esModuleInterop

I've never liked import thing = require("thing") because it looks like JavaScript instead of like a typed language. But it seems to work more consistently.