-
Notifications
You must be signed in to change notification settings - Fork 0
Modules documentation #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
74bbd09
Modules WIP
andrewbranch a89dcf8
Progress on moduleResolution
andrewbranch 48a0fe4
Add section in module resolution
andrewbranch f709c11
Add section on declaration files
andrewbranch 523ecff
WIP ESM/CJS interop
andrewbranch 92c2876
Fix mermaid in GH’s renderer
andrewbranch e2b8ed9
Progress on esModuleInterop
andrewbranch 6ee93d1
Progress on allowSyntheticDefaultImports
andrewbranch f57d860
Finish(?) esModuleInterop section
andrewbranch e76778b
WIP Node interop
andrewbranch 950ab92
Finish enumerating differences between Node ESM and transpiled ESM
andrewbranch 5b5b57b
Update outline
andrewbranch 1765cb5
Wrap up argument for esModuleInterop
andrewbranch d9a4949
Finish esModuleInterop section
andrewbranch ceeadd2
Finish common resolution features
andrewbranch fd9236e
Section on allowImportingTsExtensions
andrewbranch 03240af
Module resolution for libraries
andrewbranch 14e863a
Rename file
andrewbranch 1c9ac68
Choosing compiler options
andrewbranch c3f001f
Reference section on `module`
andrewbranch f0c32cc
WIP moduleResolution reference
andrewbranch ab6f9ae
Update modules/00_0_Outline.md
andrewbranch 61dcb01
Add `"imports"` and self-name reference section
andrewbranch f827501
Add more imports/exports examples
andrewbranch b83f74e
Add typesVersions section
andrewbranch 238bbad
Add example of exports blocking resolution
andrewbranch 992f7a4
Directory modules, extensionless paths, package-relative paths, node1…
andrewbranch be68396
Update TODOs
andrewbranch 3297cef
Update modules/01_Theory.md
andrewbranch 077fd02
Paths WIP
andrewbranch b5dc67e
Finish paths
andrewbranch 6dd61a3
Delete rootDirs section
andrewbranch 2d011ef
bundler reference section
andrewbranch 4f054b8
node10 reference section
andrewbranch cfedec8
Module syntax
andrewbranch File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
h1 { | ||
font-size: 2.5rem; | ||
} | ||
|
||
h2 { | ||
font-size: 2.2rem; | ||
} | ||
|
||
h3 { | ||
font-size: 1.8rem; | ||
} | ||
|
||
h4 { | ||
font-size: 1.4rem; | ||
} | ||
|
||
h5 { | ||
font-size: 1.1rem; | ||
} | ||
|
||
h6 { | ||
font-size: 0.9rem; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"markdown.styles": [ | ||
".vscode/md.css" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
1. Introduction | ||
1. Who is this for? | ||
1. Theory | ||
1. Scripts and modules in JavaScript | ||
1. What is TypeScript’s job concerning modules? | ||
1. Who is the host? | ||
1. Module emit | ||
1. Input syntax is (somewhat) decoupled from output | ||
1. Note: `verbatimModuleSyntax` | ||
1. Note: top-level `await` | ||
1. Module format interop | ||
1. Module specifiers are not transformed | ||
1. Module resolution | ||
1. Module resolution is host-defined | ||
1. TypeScript “mirrors” the host resolver | ||
1. Module specifiers reference the host’s source files | ||
1. Declaration files are substituted for JS files | ||
1. Note: non-JS files supported with `allowArbitraryExtensions` | ||
1. Common resolution features | ||
1. Omittable extensions and directory index files | ||
1. node_modules and package.json | ||
1. @types | ||
1. package.json `"exports"` | ||
1. Bundlers and other Node.js-like non-Node.js hosts | ||
1. `noEmit` and `allowImportingTsExtensions` | ||
1. Customization options | ||
1. Caveats / limitations? | ||
1. Why module specifiers are not transformed | ||
1. Why dual-emit is not supported and sometimes impossible | ||
1. Future work? Browser, import maps, URLs? | ||
1. Guides | ||
1. Choosing compiler options | ||
1. Troubleshooting dependency issues | ||
1. Publishing a library | ||
1. Reference | ||
1. Module syntax | ||
1. ESM (link elsewhere?) | ||
1. CJS in JavaScript | ||
1. CJS in TypeScript | ||
1. Type-only imports and exports | ||
1. Ambient module declarations & augmentations? | ||
1. `module` | ||
1. `commonjs` | ||
1. `es2015`+ | ||
1. `node16`/`nodenext` | ||
1. `moduleResolution` | ||
1. `node16`/`nodenext` | ||
1. `bundler` | ||
1. `allowJs` and `maxNodeModuleJsDepth` | ||
1. package.json | ||
1. `"main"`, `"types"` | ||
1. `"typesVersions"` | ||
1. `"exports"` | ||
1. `"type"` | ||
1. `"imports"` | ||
1. Customization compiler options | ||
1. `paths` | ||
1. `baseUrl` | ||
1. `resolvePackageJsonImports` | ||
1. `resolvePackageJsonExports` | ||
1. `customConditions` | ||
1. `allowImportingTsExtensions` | ||
1. `resolveJsonModule` | ||
1. `rootDirs` | ||
1. `typeRoots` | ||
1. `moduleSuffixes` | ||
1. Appendices | ||
1. ESM/CJS Interoperability |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# Modules in TypeScript | ||
|
||
This document is divided into three sections: | ||
|
||
1. The first section develops the **theory** behind how TypeScript approaches modules. If you want to be able to write the correct module-related tsconfig options for any situation, reason about how to integrate TypeScript with other tools, or understand how TypeScript processes dependency packages, this is the place to start. While there are guides and reference pages on these topics, building an understanding of these fundamentals will make reading the guides easier, and give you a mental framework for dealing with real-world problems not specifically covered here. | ||
2. The **guides** show how to accomplish specific real-world tasks, starting with picking the right compilation settings for a new project. The guides are a good place to start both for beginners who want to get up and running as quickly as possible and for experts who already have a good grasp of the theory but want concrete guidance on a complicated task. | ||
3. The **reference** section provides a more detailed look at the syntaxes and configurations presented in previous sections. | ||
4. The **appendices** cover complicated topics that deserve additional explanation in more detail than the theory or reference sections allow. |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
# Choosing module-related compiler options | ||
|
||
## I’m writing an app | ||
|
||
A single tsconfig.json can only represent a single environment, both in terms of what globals are available and in terms of how modules behave. If your app contains server code, DOM code, web worker code, test code, and code to be shared by all of those, each of those should have its own tsconfig.json, connected with [project references](https://www.typescriptlang.org/docs/handbook/project-references.html#handbook-content). Then, use this guide once for each tsconfig.json. For library-like projects within an app, especially ones that need to run in multiple runtime environments, use the “[I’m writing a library](#im-writing-a-library)” section. | ||
|
||
### I’m using a bundler | ||
|
||
In addition to adopting the following settings, it’s also recommended _not_ to set `{ "type": "module" }` or use `.mts` files in bundler projects for now. [Some bundlers](https://andrewbranch.github.io/interop-test/#synthesizing-default-exports-for-cjs-modules) adopt different ESM/CJS interop behavior under these circumstances, which TypeScript cannot currently analyze with `"moduleResolution": "bundler"`. See [issue #54102](https://github.com/microsoft/TypeScript/issues/54102) for more information. | ||
|
||
```json5 | ||
{ | ||
"compilerOptions": { | ||
// This is not a complete template; it only | ||
// shows relevant module-related settings. | ||
// Be sure to set other important options | ||
// like `target`, `lib`, and `strict`. | ||
|
||
// Required | ||
"module": "esnext", | ||
"moduleResolution": "bundler", | ||
"esModuleInterop": true, | ||
|
||
// Consult your bundler’s documentation | ||
"customConditions": ["module"], | ||
|
||
// Recommended | ||
"noEmit": true, // or `emitDeclarationOnly` | ||
"allowImportingTsExtensions": true, | ||
"allowArbitraryExtensions": true, | ||
"verbatimModuleSyntax": true, // or `isolatedModules` | ||
} | ||
} | ||
``` | ||
|
||
### I’m compiling and running the outputs in Node.js | ||
|
||
Remember to set `"type": "module"` or use `.mts` files if you intend to emit ES modules. | ||
|
||
```json5 | ||
{ | ||
"compilerOptions": { | ||
// This is not a complete template; it only | ||
// shows relevant module-related settings. | ||
// Be sure to set other important options | ||
// like `target`, `lib`, and `strict`. | ||
|
||
// Required | ||
"module": "nodenext", | ||
|
||
// Implied by `"module": "nodenext"`: | ||
// "moduleResolution": "nodenext", | ||
// "esModuleInterop": true, | ||
// "target": "esnext", | ||
|
||
// Recommended | ||
"verbatimModuleSyntax": true, | ||
} | ||
} | ||
``` | ||
|
||
### I’m using ts-node | ||
|
||
ts-node attempts to be compatible with the same code and the same tsconfig.json settings that can be used to [compile and run the JS outputs in Node.js](#im-compiling-and-running-the-outputs-in-node). Refer to [ts-node documentation](https://typestrong.org/ts-node/) for more details. | ||
|
||
### I’m using tsx | ||
|
||
Whereas ts-node makes minimal modifications to Node.js’s module system by default, [tsx](https://github.com/esbuild-kit/tsx) behaves more like a bundler, allowing extensionless/index module specifiers and arbitrary mixing of ESM and CJS. Use the same settings for tsx as you [would for a bundler](#im-using-a-bundler). | ||
|
||
### I’m writing ES modules for the browser, with no bundler or module compiler | ||
|
||
TypeScript does not currently have options dedicated to this scenario, but you can approximate them by using a combination of the `nodenext` ESM module resolution algorithm and `paths` as a substitute for URL and import map support. | ||
|
||
```json5 | ||
// tsconfig.json | ||
{ | ||
"compilerOptions": { | ||
// This is not a complete template; it only | ||
// shows relevant module-related settings. | ||
// Be sure to set other important options | ||
// like `target`, `lib`, and `strict`. | ||
|
||
// Combined with `"type": "module"` in a local package.json, | ||
// this enforces including file extensions on relative path imports. | ||
"module": "nodenext", | ||
"paths": { | ||
// Point TS to local types for remote URLs: | ||
"https://esm.sh/[email protected]": ["./node_modules/@types/lodash/index.d.ts"], | ||
// Optional: point bare specifier imports to an empty file | ||
// to prohibit importing from node_modules specifiers not listed here: | ||
"*": ["./empty-file.ts"] | ||
} | ||
} | ||
} | ||
``` | ||
|
||
This setup allows explicitly listed HTTPS imports to use locally-installed type declaration files, while erroring on imports that would normally resolve in node_modules: | ||
|
||
```ts | ||
import {} from "lodash"; | ||
// ^^^^^^^^ | ||
// File '/project/empty-file.ts' is not a module. ts(2306) | ||
``` | ||
|
||
Alternatively, you can use [import maps](https://github.com/WICG/import-maps) to explicitly map a list of bare specifiers to URLs in the browser, while relying on `nodenext`’s default node_modules lookups, or on `paths`, to direct TypeScript to type declaration files for those bare specifier imports: | ||
|
||
```html | ||
<script type="importmap"> | ||
{ | ||
"imports": { | ||
"lodash": "https://esm.sh/[email protected]" | ||
} | ||
} | ||
</script> | ||
``` | ||
|
||
```ts | ||
import {} from "lodash"; | ||
// Browser: https://esm.sh/[email protected] | ||
// TypeScript: ./node_modules/@types/lodash/index.d.ts | ||
``` | ||
|
||
## I’m writing a library | ||
|
||
<!-- TODO: I might move all this to a guide/appendix on library publishing and link --> | ||
|
||
Choosing compilation settings as a library author is a fundamentally different process from choosing settings as an app author. When writing an app, settings are chosen that reflect the runtime environment or bundler—typically a single entity with known behavior. When writing a library, you would ideally check your code under _all possible_ library consumer compilation settings. Since this is impractical, you can instead use the strictest possible settings, since satisfying those tends to satisfy all others. | ||
|
||
```json5 | ||
{ | ||
"compilerOptions": { | ||
"module": "node16", | ||
"target": "es2020", // set to the *lowest* target you support | ||
"strict": true, | ||
"verbatimModuleSyntax": true, | ||
"declaration": true, | ||
"sourceMap": true, | ||
"declarationMap": true | ||
} | ||
} | ||
``` | ||
|
||
Let’s examine why we picked each of these settings: | ||
|
||
- **`module: "node16"`**. When a codebase is compatible with Node.js’s module system, it almost always works in bundlers as well. If you’re using a third-party emitter to emit ESM outputs, ensure that you set `"type": "module"` in your package.json so TypeScript checks your code as ESM, which uses a stricter module resolution algorithm in Node.js than CommonJS does. | ||
- **``target: "es2020"``**. Setting this value to the _lowest_ ECMAScript version that you intend to support ensures the emitted code will not use language features introduced in a later version. Since `target` also implies a corresponding value for `lib`, this also ensures you don’t access globals that may not be available in older environments. | ||
- **`strict: true`**. Without this, you may write type-level code that ends up in your output `.d.ts` files and errors when a consumer compiles with `strict` enabled. For example, this `extends` clause: | ||
```ts | ||
export interface Super { | ||
foo: string; | ||
} | ||
export interface Sub extends Super { | ||
foo: string | undefined; | ||
} | ||
``` | ||
is only an error under `strictNullChecks`. On the other hand, it’s very difficult to write code that errors only when `strict` is _disabled_, so it’s highly recommended for libraries to compile with `strict`. | ||
- **`verbatimModuleSyntax: true`**. This setting protects against a few module-related pitfalls that can cause problems for library consumers. First, it prevents writing any import statements that could be interpreted ambiguously based on the user’s value of `esModuleInterop` or `allowSyntheticDefaultImports`. Previously, it was often suggested that libraries compile without `esModuleInterop`, since its use in libraries could force users to adopt it too. However, it’s also possible to write imports that only work _without_ `esModuleInterop`, so neither value for the setting guarantees portability for libraries. `verbatimModuleSyntax` does provide such a guarantee.[^1] Second, it prevents the use of `export default` in modules that will be emitted as CommonJS, which can require bundler users and Node.js ESM users to consume the module differently. See the appendix on [ESM/CJS Interop](./04_01_ESM-CJS-Interop.md#library-code-needs-special-considerations) for more details. | ||
- **`declaration: true`** emits type declaration files alongside the output JavaScript. This is needed for consumers of the library to have any type information. | ||
- **`sourceMap: true`** and **`declarationMap: true`** emit source maps for the output JavaScript and type declaration files, respectively. These are only useful if the library also ships its source (`.ts`) files. By shipping source maps and source files, consumers of the library will be able to debug the library code somewhat more easily. By shipping declaration maps and source files, consumers will be able to see the original TypeScript sources when they run Go To Definition on imports from the libraries. Both of these represent a tradeoff between developer experience and library size, so it’s up to you whether to include them. | ||
|
||
### Considerations for bundling libraries | ||
|
||
If you’re using a bundler to emit your library, then all your (non-externalized) imports will be processed by the bundler with known behavior, not by your users’ unknowable environments. In this case, you can use `"module": "esnext"` and `"moduleResolution": "bundler"`, but only with a significant caveat: you must ensure that your declaration files get bundled as well. Recall the [first rule of declaration files](./01_Theory.md#the-role-of-declaration-files): every declaration file represents exactly one JavaScript file. If you use `"moduleResolution": "bundler"` and use a bundler to emit an ESM bundle while using `tsc` to emit many individual declaration files, your declaration files may cause errors when consumed under `"module": "nodenext"`. For example, an input file like: | ||
|
||
```ts | ||
import { Component } from "./extensionless-relative-import"; | ||
``` | ||
|
||
will have its import erased by the JS bundler, but produce a declaration file with an identical import statement. That import statement, however, will contain an invalid module specifier in Node.js, since it’s missing a file extension. For Node.js users, TypeScript will error on the declaration file and infect types referencing `Component` with `any`, assuming the dependency will crash at runtime. | ||
|
||
### Notes on dual-emit solutions | ||
|
||
A single TypeScript compilation (whether emitting or just type checking) assumes that each input file will only produce one output file. Even if `tsc` isn’t emitting anything, the type checking it performs on imported names rely on knowledge about how the output file will behave at runtime, based on the module- and emit-related options set in the tsconfig.json. While third-party emitters are generally safe to use in combination with `tsc` type checking as long as `tsc` can be configured to understand what the other emitter will emit, any solution that emits two different sets of outputs with different module formats while only type checking once leaves (at least) one of the outputs unchecked. Because external dependencies may expose different APIs to CommonJS and ESM consumers, there’s no configuration you can use to guarantee in a single compilation that both outputs will be type-safe. In practice, most dependencies follow best practices and dual-emit outputs work. Running tests and [static analysis](https://npmjs.com/package/@arethetypeswrong/cli) against all output bundles before publishing significantly reduces the chance of a serious problem going unnoticed. | ||
|
||
[^1]: `verbatimModuleSyntax` can only work when the JS emitter emits the same module kind as `tsc` would given the tsconfig.json, source file extension, and package.json `"type"`. The option works by enforcing that the `import`/`require` written is identical to the `import`/`require` emitted. Any configuration that produces both an ESM and a CJS output from the same source file is fundamentally incompatible with `verbatimModuleSyntax`, since its whole purpose is to prevent you from writing `import` anywhere that a `require` would be emitted. `verbatimModuleSyntax` can also be defeated by configuring a third-party emitter to emit a different module kind than `tsc` would—for example, by setting `"module": "esnext"` in tsconfig.json while configuring Babel to emit CommonJS. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would consider IM "soft deprecated" in favor of VMS, so maybe worth downplaying