You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: book-content/chapters/14-configuring-typescript.md
+213-2Lines changed: 213 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -72,10 +72,11 @@ Based on your answers to the above questions, here's how the complete `tsconfig.
72
72
"strict": true,
73
73
"noUncheckedIndexedAccess": true,
74
74
75
-
/* If transpiling with TypeScript: */
75
+
/* If transpiling with tsc: */
76
76
"module": "NodeNext",
77
77
"outDir": "dist",
78
78
"sourceMap": true,
79
+
"verbatimModuleSyntax": true,
79
80
80
81
/* AND if you're building for a library: */
81
82
"declaration": true,
@@ -84,7 +85,7 @@ Based on your answers to the above questions, here's how the complete `tsconfig.
84
85
"composite": true,
85
86
"declarationMap": true,
86
87
87
-
/* If NOT transpiling with TypeScript: */
88
+
/* If NOT transpiling with tsc: */
88
89
"module": "Preserve",
89
90
"noEmit": true,
90
91
@@ -363,6 +364,216 @@ This is because the bundler will take care of resolving the file paths and exten
363
364
364
365
This means that if you're using an external bundler or transpiler, you should use `module: "Preserve"` in your `tsconfig.json` file. This is also true if you're using a frontend framework like Next.js, Remix, Vite, or SvelteKit - it will handle the bundling for you.
365
366
367
+
## Importing Types With `import type`
368
+
369
+
When you're importing types from other files, TypeScript has some choices to make. Let's say you're importing a type of `Album` from `album.ts`:
370
+
371
+
```typescript
372
+
// index.ts
373
+
374
+
import { Album } from"./album";
375
+
```
376
+
377
+
What should the emitted JavaScript look like? We're only importing a type, which disappears at runtime. Should the import remain, but with the type removed?
378
+
379
+
```javascript
380
+
// index.js
381
+
382
+
import {} from"./album";
383
+
```
384
+
385
+
Or should the import be removed entirely?
386
+
387
+
These decisions matter, because modules can contain effects which run when they're first imported. For instance, `album.ts` might call a `console.log` statement:
388
+
389
+
```typescript
390
+
// album.ts
391
+
392
+
exportinterfaceAlbum {
393
+
title:string;
394
+
artist:string;
395
+
year:number;
396
+
}
397
+
398
+
console.log("Imported album.ts");
399
+
```
400
+
401
+
Now, if TypeScript removes (or, as they say in the TypeScript docs, elides) the import, the `console.log` statement won't run. This can be surprising if you're not expecting it.
402
+
403
+
The way TypeScript resolves this is with the `import type` syntax. If you're importing a type and you don't want the import to be emitted in the JavaScript, you can use `import type`:
404
+
405
+
```typescript
406
+
// index.ts
407
+
408
+
importtype { Album } from"./album";
409
+
```
410
+
411
+
Now, only the type information is imported, and the import is removed from the emitted JavaScript:
412
+
413
+
```javascript
414
+
// index.js
415
+
416
+
// No import statement
417
+
```
418
+
419
+
### `import type { X }` vs `import { type X }`
420
+
421
+
You can combine `import` and `type` in two ways. You can either mark the entire line as a type import:
422
+
423
+
```typescript
424
+
importtype { Album } from"./album";
425
+
```
426
+
427
+
Or, if you want to combine runtime imports with type imports, you can mark the type itself as a type import:
428
+
429
+
```typescript
430
+
import { typeAlbum, createAlbum } from"./album";
431
+
```
432
+
433
+
In this case, `createAlbum` will be imported as a runtime import, and `Album` will be imported as a type import.
434
+
435
+
In both cases, it's clear what will be removed from the emitted JavaScript. The first line will remove the entire import, and the second line will remove only the type import.
436
+
437
+
### `verbatimModuleSyntax` Enforces `import type`
438
+
439
+
TypeScript has gone through various iterations of configuration options to support this behavior. `importsNotUsedAsValues` and `preserveValueImports` both tried to solve the problem. But since TypeScript 5.0, `verbatimModuleSyntax` is the recommended way to enforce `import type`.
440
+
441
+
The behavior described above, where imports are elided if they're only used for types, is what happens when `verbatimModuleSyntax` is set to `true`.
442
+
443
+
## ESM and CommonJS
444
+
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
+
exportfunction 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
+
exportfunctioncreateAlbum(title, artist, year) {
487
+
return { title, artist, year };
488
+
}
489
+
```
490
+
491
+
```javascript
492
+
// CJS
493
+
494
+
functioncreateAlbum(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.
508
+
509
+
If we change `album.ts` to `album.cts`, TypeScript will emit CommonJS syntax, and the emitted file extension will be `.cjs`.
510
+
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.
|`album.ts`|`album.js`| Depends on `type` in `package.json`|
520
+
521
+
### `verbatimModuleSyntax` With ESM and CommonJS
522
+
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:
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.
555
+
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
+
importzod=require("zod");
569
+
570
+
const z =zod.z;
571
+
```
572
+
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.
574
+
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`.
576
+
366
577
## `noEmit`
367
578
368
579
The `noEmit` option in `tsconfig.json` tells TypeScript not to emit any JavaScript files when transpiling your TypeScript code.
0 commit comments