Check out a free preview of the full Intermediate TypeScript, v2 course

The "CommonJS Interop" Lesson is part of the full, Intermediate TypeScript, v2 course featured in this preview video. Here's what you'd learn in this lesson:

Mike explains the differences between CommonJS modules and ECMAScript modules and demonstrates how to import and export modules using both styles. Potential issues and limitations of using CommonJS modules and some alternative solutions are also discussed in this segment.


Transcript from the "CommonJS Interop" Lesson

>> You will inevitably need to work with modules that are shipped, that are distributed as CommonJS modules. And what do we mean by CommonJS? So I have one in here, banana.js, I bet. That's not CommonJS, is Melon? No, it is, sorry, Banana is. We have a class called Banana, and then this is the smoking gun here.

This is the key thing that tells us it's CommonJS, module.exports = { Banana }. So, this is the way Node.js has treated JS files since its inception, as far as I'm aware. You could point Node.js at this today in a file with a JS extension, and it would be totally happy to run this.

So how do we deal with files like that? Well, look, we're consuming Banana over here. All these fruit things and talking about consuming them is making me hungry. So here we're importing from this banana file, and we're importing * as bananaNamespace. So here we've got bananaNanamespace.Banana. In fact, you could also just do this.

You could just do it conventionally. As long as it's a named export, You could do this. When I say a named export, I mean as long as it's sort of within an object like this. It's one of many things that could be exported. You could put a comma here and export a bunch of other stuff if you wanted to, right?

But namespace importing is a valid thing to do. Has anyone here ever modernized a Node.js library or something like that, where you're reaching for the fs module? It used to be just you do const fs = require fs as string. This is what you'd convert it to. Something like that, right?

And why might you do it this way? Well, maybe in a bunch of different places in your module, you're referring to this fs thing. And you can just make this one change, you don't have to perturb a bunch of code that already assumes the existence of an fs.

You change this import, you go about your day. It's a relatively low-risk code change, compared to refactoring everything around so you're doing specific imports in specific places. Okay, let's look at something else. Let's say we have to import something as a single thing. So, Melon is also a CommonJS module, but we're getting an error here.

And if we look at Melon, let's go look at what melon.js has in it. Look at that, there's no braces here. We're exporting Melon as a single thing. Sometimes you'll see a function here, right? We can't do a named import for this. This isn't a default export, right?

This isn't a default export, it's not a named export. It's just saying Melon is all the exported stuff, right? There's no picking apart individual things. It's the one thing that we're exporting here. Not to be confused with the default export. Cuz remember, with modern JavaScript modules, you can have a default export and also named exports all in the same file.

They don't collide with each other, it's fine. But here we're exporting one thing, and it's sort of hogging up all the space here. So we're getting an error message. And the error message says, the module can only be referenced with ECMAScript imports/exports by turning on the 'esModuleInterop' flag and referencing its default export.

We could follow that advice. But we're not going to. And here's why. If you turn on that esModuleInterop flag, TypeScript is gonna helpfully, behind the scenes, say, you know what? I'm gonna treat that Melon thing as if it's a default export. And I'm gonna type check a little bit differently, such that you're not going to have a problem here.

And then, let's say we have a library, and we're gonna emit types that still have that kind of problematic thing in them. But it's okay, as long as it's type checked, with esModuleInterop turned on. So I call this a viral option. If you enable this in a library, and there are other viral options, we'll talk about those in the Enterprise TS course.

But if you enable this in a library, you are forcing all consumers of that library to also enable this, or their project will not type check. And as a library author, I think that's a bad thing. I want to leave optionality for consumers of my code, I want to let them make the right decision for them.

Plenty of library consumers will turn this flag on. If you're building an app, of course, you can turn this flag on, that's your app, who cares? But I don't wanna force somebody to shift into a different mode that will affect their type checking just because I wanna solve this problem a particular way.

And there's another way to solve it that doesn't require us imposing things on consumers of our code. And here's what we have to do. And the error message, if we look closely at it, there's a little wiggle room here. And it's here. This module can be only referenced with ECMAScript imports/exports, if we turn this flag on.

But there's a way to reference this module without ECMAScript imports and exports. And it's using an old style of TypeScript importing that predates ES modules being a standardized thing. And that's this syntax here. It's really weird, this does not work in JavaScript, import Melon = require ('./melon'). If we look at that, well, there's the class.

And we can use the Melon constructor, we can cut it into slices, everything type checks here. So I know that the TypeScript core team, they're trying to steer people in the direction of, we're trying to use ES modules and things like that. But this is a useful escape hatch to know about, where if you didn't wanna change the way your project is compiled, you could still import in this way.

It'll compile to a totally runnable thing, it's just not an ECMAScript import. It's a different kind of import, a specific way that TypeScript lets you import things. It looks a little bit like this. Almost a CJS import, but we're using the import keyword here. Rohit says, when you use the syntax in React, then it'll give an error.

I'm not sure I believe that that is always true. I think it's likely you should be able to adjust your compiler settings to allow this to work. Yeah, LJ in chat says it seems weird that we have so many different options for import, export, it seems a little confusing.

I agree, this is why we're talking about it. We're in kind of a transitionary state where old code exists, new code also exists. We probably have to consume a variety of both. And we need to be able to manage that, both in library and app code. So this is part of the reason why it's worthwhile to focus on this.

>> Will that compile to CommonJS or ES modules, depending on what you set the-
>> Yeah, let's let's go look. So I'm gonna clear this out, and I'm gonna say, Actually, I can do this style of import with any CommonJS module, even if it's not that special type where this is sort of a last resort.

So I could still do that. Now, I don't have to, there's a better option here, there's the wildcard import. And look what it compiles to, perfectly fine CommonJS. It is literally the same thing as this, Except the key difference here, and by the way, with this import here, look at what it's having to do, well, cuz we're compiling out to CJS.

So it has to put some poly filling in there to make that possible. Here's a little thing have you do default exports, things like that. But the important thing at the bottom is we should just see a require. Sorry, __importStar(require), right? So this is actually the real import, and then it's sort of processing what the require returns into something that has a default and all that kind of stuff.

But the important thing is, in terms of how the import is performed, it's very, very similar. The thing that gives us a little bit extra flexibility here is, in the wildcard import case, this thing has to be namespace-ish. You have to be able to sort of access several independent things on it.

If we looked at our Banana module here, this is a namespace-ish thing. There's a property called Banana with a capital B, and its value is the Banana class. That's a namespace thing, right? It's an object that has some specific things that we could pick off of it and use.

In our Melon class, that's not the case. So you can almost think of this type of import, in terms of what's happening behind the scenes, as a special case of this, right? This works even if it's not namespace-like. The only time I reach for this is when no other options will do.

I would prefer to use ECMAScript imports wherever I can. But I would prefer to use this, especially in library code, over forcing everyone who uses my library to change the way their imports work. That seems like an imposition.

Learn Straight from the Experts Who Shape the Modern Web

  • In-depth Courses
  • Industry Leading Experts
  • Learning Paths
  • Live Interactive Workshops
Get Unlimited Access Now