Skip to content

Commit c9aaf97

Browse files
committed
Finished review of 14
1 parent f7bb6de commit c9aaf97

File tree

1 file changed

+125
-12
lines changed

1 file changed

+125
-12
lines changed

book-content/chapters/14-configuring-typescript.md

Lines changed: 125 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,11 @@ Based on your answers to the above questions, here's how the complete `tsconfig.
7272
"strict": true,
7373
"noUncheckedIndexedAccess": true,
7474

75-
/* If transpiling with TypeScript: */
75+
/* If transpiling with tsc: */
7676
"module": "NodeNext",
7777
"outDir": "dist",
7878
"sourceMap": true,
79+
"verbatimModuleSyntax": true,
7980

8081
/* AND if you're building for a library: */
8182
"declaration": true,
@@ -84,7 +85,7 @@ Based on your answers to the above questions, here's how the complete `tsconfig.
8485
"composite": true,
8586
"declarationMap": true,
8687

87-
/* If NOT transpiling with TypeScript: */
88+
/* If NOT transpiling with tsc: */
8889
"module": "Preserve",
8990
"noEmit": true,
9091

@@ -439,27 +440,139 @@ TypeScript has gone through various iterations of configuration options to suppo
439440

440441
The behavior described above, where imports are elided if they're only used for types, is what happens when `verbatimModuleSyntax` is set to `true`.
441442

442-
You might be wondering why it isn't part of the recommended set of options detailed above. The reason is that it also has some impact on how modules work in TypeScript - we'll talk about that in the next section.
443-
444443
## ESM and CommonJS
445444

446-
<!-- TODO -->
445+
There are two ways to modularize your code in TypeScript: ECMAScript Modules (ESM) and CommonJS (CJS). These two module systems operate slightly differently, and they don't always work together cleanly.
446+
447+
ES Modules use `import` and `export` statements:
448+
449+
```typescript
450+
import { createAlbum } from "./album";
451+
452+
export { createAlbum };
453+
```
454+
455+
CommonJS uses `require` and `module.exports`:
456+
457+
```typescript
458+
const { createAlbum } = require("./album");
459+
460+
module.exports = { createAlbum };
461+
```
462+
463+
Understanding the interoperability issues between ESM and CJS is a little beyond the scope of this book. Instead, we'll look at how to set up TypeScript to make working with both module systems as easy as possible.
464+
465+
### How Does TypeScript Know What Module System To Emit?
466+
467+
Imagine we have our `album.ts` file that exports a `createAlbum` function:
468+
469+
```typescript
470+
// album.ts
471+
472+
export function createAlbum(
473+
title: string,
474+
artist: string,
475+
year: number,
476+
): Album {
477+
return { title, artist, year };
478+
}
479+
```
480+
481+
When this file is turned into JavaScript, should it emit `CJS` or `ESM` syntax?
482+
483+
```javascript
484+
// ESM
485+
486+
export function createAlbum(title, artist, year) {
487+
return { title, artist, year };
488+
}
489+
```
490+
491+
```javascript
492+
// CJS
493+
494+
function createAlbum(title, artist, year) {
495+
return { title, artist, year };
496+
}
497+
498+
module.exports = {
499+
createAlbum,
500+
};
501+
```
502+
503+
The way this is decided is via `module`. You can hardcode this by choosing some older options. `module: CommonJS` will always emit CommonJS syntax, and `module: ESNext` will always emit ESM syntax.
504+
505+
But if you're using TypeScript to transpile your code, I recommend using `module: NodeNext`. This has several complex rules built-in for understanding whether to emit CJS or ESM:
506+
507+
The first way we can influence how TypeScript emits your modules with `module: NodeNext` is by using `.cts` and `.mts` extensions.
447508

448-
### `type` In `package.json` And `module: NodeNext`
509+
If we change `album.ts` to `album.cts`, TypeScript will emit CommonJS syntax, and the emitted file extension will be `.cjs`.
449510

450-
<!-- TODO -->
511+
If we change `album.ts` to `album.mts`, TypeScript will emit ESM syntax, and the emitted file extension will be `.mjs`.
512+
513+
If we keep `album.ts` the same, TypeScript will look up the directories for the closest `package.json` file. If the `type` field is set to `module`, TypeScript will emit ESM syntax. If it's set to `commonjs` (or unset, matching Node's behavior), TypeScript will emit CJS syntax.
514+
515+
| File Extension | Emitted File Extension | Emitted Module System |
516+
| -------------- | ---------------------- | ----------------------------------- |
517+
| `album.mts` | `album.mjs` | ESM |
518+
| `album.cts` | `album.cjs` | CJS |
519+
| `album.ts` | `album.js` | Depends on `type` in `package.json` |
451520

452521
### `verbatimModuleSyntax` With ESM and CommonJS
453522

454-
<!-- TODO -->
523+
`verbatimModuleSyntax` can help you be more explicit about which module system you're using. If you set `verbatimModuleSyntax` to `true`, TypeScript will error if you try to use `require` in an ESM file, or `import` in a CJS file.
524+
525+
For example, consider this file `hello.cts` that uses the `export default` syntax:
526+
527+
```tsx
528+
// hello.cts
529+
const hello = () => {
530+
console.log("Hello!");
531+
};
532+
533+
export { hello }; // red squiggly line under export { hello }
534+
```
535+
536+
When `verbatimModuleSyntax` is enabled, TypeScript will show an error under the `export default` line that tells us we're mixing the syntaxes together:
455537

456-
#### Importing and Exporting In CJS
538+
```tsx
539+
// hovering over export { hello } shows:
540+
ESM syntax is not allowed in a CommonJS module when 'verbatimModuleSyntax' is enabled.
541+
```
542+
543+
In order to fix the issue, we need to use the `export =` syntax instead:
544+
545+
```tsx
546+
// hello.cts
547+
548+
const hello = () => {
549+
console.log("Hello!");
550+
};
551+
export = { hello };
552+
```
553+
554+
This will compile down to `module.exports = { hello }` in the emitted JavaScript.
457555

458-
`import X = require('x')`
556+
The warnings will show when trying to use an ESM import as well:
557+
558+
```tsx
559+
import { z } from "zod"; // rsl under import statement
560+
561+
// hovering over the import shows:
562+
// ESM syntax is not allowed in a CommonJS module when 'verbatimModuleSyntax' is enabled.
563+
```
564+
565+
Here, the fix is to use `require` instead of `import`:
566+
567+
```tsx
568+
import zod = require("zod");
569+
570+
const z = zod.z;
571+
```
459572

460-
<!-- TODO -->
573+
Note that this syntax combines `import` and `require` in a curious way - this is a TypeScript-specific syntax that gives you autocomplete in CommonJS modules.
461574

462-
#### When `verbatimModuleSyntax` Isn't Appropriate
575+
`verbatimModuleSyntax` is a great way to catch these issues early, and to make sure you're using the right module system in the correct files. It pairs very well with `module: NodeNext`.
463576

464577
## `noEmit`
465578

0 commit comments

Comments
 (0)