Configuring TypeScript 17 exercises
explainer

NodeNext vs. Preserve Module Resolution in TypeScript

Let's look at the differences between "module": "NodeNext" and "module": "preserve" in TypeScript's tsconfig.json file.

Node-style Module Resolution with NodeNext

Setting module to NodeNext in your tsconfig.json file instructs TypeScript to emulate the Node.js module resolution str

Loading explainer

Transcript

00:00 Let's look at the difference between module node next and module preserve. Module preserve implies a module resolution of bundler here. Now module node next, what it does is it forces you whenever you use it. So is we essentially have an index.ts here, which is importing something from example,

00:17 and it forces you to use .js extensions here. Whereas if we go back to our tsconfig.json and move it to module preserve instead, now it doesn't require these. So you can actually omit this. So why does module node next actually require them?

00:36 And let's actually have a look at the error here. For instance, if we go back to tsconfig.json, push this back to module node next, now we're going to get an error saying relative import paths need explicit file extensions in ECMAScript imports. So why is it doing this? Well, first of all,

00:53 we saw in the mjs cjs stuff that we were looking at earlier that file extensions really do matter. And in our setup here, we just have two TypeScript files, but we have a type of module here. And so we're using ECMAScript imports, or that's what we're producing in the end. And we can see that by looking at the example.js here and index.js.

01:13 So again, we've just got import example from example. Now if we look at this import example from example, how does the, how does node when it runs these files know exactly which file it should be running here? If we had an example.mjs, example.cjs, this might be kind of problematic.

01:32 And so we actually do explicitly need to say which file extension we use here because we could have multiple different ones. And so example.js is what we need. And if we look at the outputted JavaScript here, we can see that we get example.js being outputted. Very, very nice.

01:50 But you might think that's really weird. If we look at this and we just take a step back, why aren't we doing example.ts here? Because that's the file that we've actually got in our source directory. That's the one that we're targeting here. This is really, really weird. And import path can only end in a ts extension when allow importing ts

02:09 extensions is enabled. Well, this is I think a choice by the TypeScript team. It's basically saying that the imports you write are not going to be transformed by TypeScript. It's not going to be transformed at all. And it's basically saying, look,

02:24 it's a lot simpler if you see the imports that are going to be outputted. And so, yeah, you're kind of targeting a file that doesn't exist in your source directory, but it means that the code that you write, I like to think of my TypeScript as just TypeScript or just JavaScript with a few little sprinkles on top.

02:42 And really the thing that I'm outputting here is JavaScript. So it does force me in a way to ensure that my, my imports and exports are written correctly. Now, let's though look at module preserve because module preserve, all it's doing here is it's basically saying don't mess with the imports and exports.

03:02 You don't need to because that's going to be handled by an external bundler. So module preserve should be chosen when another bundler is transpiling your code for you because that bundler is going to do the work of resolving the .js extensions, .mjs extensions. It's going to figure that out for you. So you just don't need to worry.

03:20 So this means that you don't need .js extensions here. And this has been the this has been the convention for a long time, for many, many different stacks is basically not requiring these .js extensions. And this means that, yeah, import example from example will just work like this and you will get the right

03:40 result because the bundler is handling it for you. Essentially it gets to be more opinionated than TypeScript wants to be. So this is really what you should think about here is that when you're using an external bundler, use module preserve. And when you're using TSC to transpile use module node next.

03:58 And you do get a different behavior in terms of the .js extensions. You don't need them for when you're using a module resolution bundler and module preserve. But yeah, this is the kind of trade off here. If you try to run this code like if you try to run code that's

04:14 used module preserve with node, I'm actually not quite sure of the output. I'm pretty sure it won't work and actually using module node next when you're targeting node is actually what you need to do. So that's the difference between module preserve and module node next.