The JS ecosystem was without an “official” module specification until 2015, which led to a variety of different community-defined module types, including…
AMD and UMD modules are increasingly rare these days, but CommonJS has stuck around, in part due to it still being the default module type for code that runs in Node.js.
While it’s unusual that we write anything other than “ES modules” these days, it’s very common to need to describe the types of older CJS code.
ES Module imports and exports
First, let’s get the conventional stuff out of the way: TypeScript does exactly what you’re used to seeing in modern JavaScript code.
Here are some of the basics:
ts
// named importsimport { strawberry, raspberry } from "./berries"import kiwi from "./kiwi" // default importexport function makeFruitSalad() {} // named exportexport default class FruitBasket {} // default exportexport { lemon, lime } from "./citrus"
Although fairly uncommon in the JS world, it’s possible to import an entire module as a namespace. TypeScript supports this as well
ts
import * as allBerries from "./berries" // namespace importallBerries.strawberry // using the namespaceallBerries.blueberryallBerries.raspberryexport * from "./berries" // namespace re-export
TypeScript also allows something that was recently added (2021) to the JS language
ts
export * as berries from "./berries" // namespace re-export
CommonJS Interop
Things can sometimes get a bit tricky when consuming CommonJS modules that do things that are incompatible with the way ES Modules typically work.
Most of the time, you can just convert something like
js
const fs = require("fs")
into
ts
// namespace importimport * as fs from "fs"
but occasionally, you’ll run into a rare situation where the CJS module you’re importing from, exports a single thing that’s incompatible with this namespace import technique.
Here’s a small example of where the namespace import fails:
tsTry
////////////////////////////////////////////////////////// @filename: fruits.tsThis module can only be referenced with ECMAScript imports/exports by turning on the 'esModuleInterop' flag and referencing its default export.2497This module can only be referenced with ECMAScript imports/exports by turning on the 'esModuleInterop' flag and referencing its default export.functioncreateBanana () {return {name : "banana",color : "yellow",mass : 183 }}// equivalent to CJS `module.exports = createBanana`export =createBanana ////////////////////////////////////////////////////////// @filename: smoothie.tsimport * ascreateBanana from "./fruits"
While this error message is accurate, you may not want to follow the advice it provides in all situations.
If you need to enable the
esModuleInterop
andallowSyntheticDefaultImports
compiler flags in order to allow your types to compile, anyone who depends on your types will also have no choice but to enable them.
I call these “viral options”, and take extra steps to avoid using them in my libraries.
Thankfully we have another option here — the use of an older module loading API that imports the code properly, and matches up the type information as well
tsTry
////////////////////////////////////////////////////////// @filename: fruits.tsfunctioncreateBanana () {return {name : "banana",color : "yellow",mass : 183 }}// equivalent to CJS `module.exports = createBanana`export =createBanana ////////////////////////////////////////////////////////// @filename: smoothie.tsimportcreateBanana = require("./fruits")constbanana =createBanana ()
The error message said
This module can only be referenced with ECMAScript imports/exports by turning on the ‘esModuleInterop’ flag
and we have solved this by avoiding the use of an ECMAScript import/export. After all, the code we’re referring to here is not following the ES module spec to begin with
The compiled output of this file will still be what we’re looking for in the CJS world
tsTry
"use strict";function createBanana() {return { name: "banana", color: "yellow", mass: 183 };}module.exports = createBanana;////////////////////////////////////////////////////////
tsTry
"use strict";Object.defineProperty(exports, "__esModule", { value: true });var createBanana = require("./fruits");var banana = createBanana();
The type information you publish could be downloaded into a user’s
authoring environment, even if they don’t directly consume your library
Type-checking is a holistic operation that can be upset by
even one dependency whose types are “unhappy”
Importing non-TS things
Particularly if you use a bundler like webpack, parcel or snowpack, you
may end up importing things that aren’t .js
or .ts
files
For example, maybe you’ll need to import an image file with webpack like this
tsTry
importCannot find module './file.png' or its corresponding type declarations.2307Cannot find module './file.png' or its corresponding type declarations.img from"./file.png"
file.png
is obviously not a TypeScript file — we just need
to tell TypeScript that whenever we import a .png
file,
it should be treated as if it’s a JS module with a string
value as its default export
This can be accomplished through a module declaration as shown below
tsTry
// @filename: global.d.tsdeclare module "*.png" {constimgUrl : stringexport defaultimgUrl }// @filename: component.tsimportimg from "./file.png"
Like an interface, this is purely type information that will “compile away” as part of your build process. We’ll talk more about module declarations when we discuss ambient type information