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

The "Native ES Modules" 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 discusses that TypeScript supports native ES modules and introduces the different file extensions that can be used for ES modules and CommonJS modules. How to specify the module type in the package.json file and how to import a CommonJS module in TypeScript is also demonstrated in this segment.


Transcript from the "Native ES Modules" Lesson

>> Let's talk about native ES modules. So, TypeScript supports native ES modules and what we're gonna do here, I'm gonna just make this narrower cuz we're gonna need some stuff there. So Let's pretend we want all files in this project to be treated as ES modules. And before we go into exactly how that would work, I wanna point out that there are new file extensions that are part of the way node handles modules.

I think this is standardized, and if it's not, it's on its way to being standardized. You can have, I actually have a web page for this, which is easier to look at than code. Right here, okay, so, We have different kinds of file extensions we can use, .njs, .cjs.

Has anyone ever used a .tsx file? TypeScript JSX file? Yep, so same convention applies here, swap the j for a t. You can have an empty s file or a TS file. And these files are unambiguous. An .mjs or a .mts file, that is a native ES module for sure.

It's an ES module, should be treated as such. Files with the .cjs or .cts extension, unambiguously, are CommonJS modules. And you can actually mix these together in a project and TypeScript knows how to handle both. They'll all sort of be compiled together and you'll get a consistent result.

So if you have some CommonJS code in your project, you could just rename the files and it's unambiguous. Now, what happens with the .js files? Well, you have a couple options, right? You have two options. You could start with the default, which is they're not assumed to be ES modules.

And by the way, when I talk about native ES modules, it's not just about this syntax here, there are features like top-level await, which you kinda need to exist. It can't be polyfilled very easily and compiled down to some CJS equivalent. So there are some features of native ES modules that are not just about imports and exports, but what you're allowed to do in them.

And this is relatively unusual stuff. I mean, it's new stuff. But there are reasons why you might want to say, all right, this is not just using import and export modern syntax, but treat this as the standardized module. Now I can have top-level awaits. I don't need to have a main async function that I then invoke, I can have top-level awaits and top-level module scope.

So these are the unambiguous file extensions, and then we can make decisions about how we want .js files to be treated. And there is a top-level type field in your package.json, which you can use to state what should be assumed about the .js files. If you use module, that means .js files should be assumed to be ES modules, they should be run as ES modules.

If you have CommonJS that means that they should be treated as CommonJS modules. So you have those options. So, let's make a little change in our project. We can take Banana and make it a CJS. I'm not gonna update imports because I want you to see me updating imports.

Wow, I hit Never, that's a setting that's gonna haunt me, I'll figure out where that is and undo it. Okay, so now we've got bananaNamespace. Let's call it bananaNamespace2. We can import it from banana.cjs. Let's look at our existing banana import. Look, that has failed. So when we're importing a CJS file, we need to add an extension, and it'll work perfectly fine.

This is gonna have. Our Banana class, it's all there. But this is a good thing to know about, especially if you're writing Node.js code. That's the more likely place where you may see a lot of existing CJS stuff because it has technically been possible to have Node run modern JavaScript modules.

But until somewhat recent releases, I think I had Node 13 written down as the transition point. You had to operate Node in --harmony mode and you had to sort of opt into a bunch of experimental behavior to get it to work now it works natively. And so you you might come across places where you have to mix these things.

And this is a great thing to help keep this straight. And of course, it's also great to be able to apply lint rules that are a little bit different for CJS stuff versus native ES module stuff, you might have different roles that you wanna enforce. And a different file extension makes that super, super easy.

Rohit had a great question here. What does top-level await mean? It means this. I can't do this, cuz I'm not operating in a sufficiently modern module version right? What it's saying is. First of all, the target option needs to be set to a particularly modern version of JavaScript, ES2017.

I happen to know it's yelling at me, or that error message is here. It's not really an error in this piece of code, but it's part of this error message. Await in general is necessary, or it landed in ES2017, right? And then here, it's saying Top-level 'await' expressions are only allowed when the 'module' option is set to 'es2022'.

That means I'm spitting out ES2022 modules instead of CJS modules I'm emitting those. And a corollary of this, or a reason behind this is, it's just not possible to port this over to CJS in a reasonable way. The workaround of course is I'm sure, oops, I need async function.

You could always do this, Something like that, or you could even have an async immediately invoke function expression, something like this. But it's not really the same. This will also work, but again, in both of these cases, we're absolutely not having a top-level await. Whether we have a named function we're invoking, or it's an IIFE, you're creating a scope in which the await exists.

So top-level await, it's not something that can compile down to CommonJS. And that's why TypeScript's saying you can only do this If you output a sufficiently modern module format, not a language level, but the kind of module. So if we go back to the TypeScript playground, we can see what that looks like.

That would be like us going here, module ES 2022, and there we go. It's decided to drop the import entirely, which is interesting. I can't do this. Yeah, it's not even letting me do this in ES22. So I'd have to do the namespace. But in this scenario, sorry, I'm getting my syntaxes confused.

In this scenario, you're not really worried about CommonJS interrupt because it's not just a choice about, I have one piece of code that can be emitted in a variety of different module formats. When you use a top-level await, that is something that only is supported in certain module formats, the most modern ones.

And one day we'll build this, this will be our build target and we'll run things this way. But honestly, there's not really a great reason to spit out modules that are this modern, right? It's a compiled target, aside from top-level await, which you'd have to think about, is that something you really, really need?

You don't really get much difference between a CGS compiled target or these modern modules. It's the code that is actually being run. And remember, you're already compiling, you're ripping out the types from your code. And so aside from those very small set of features, there will certainly be more in the future, but there's no advantage to outputting super super modern stuff.

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