Skip to content

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
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
74bbd09
Modules WIP
andrewbranch Apr 3, 2023
a89dcf8
Progress on moduleResolution
andrewbranch Apr 3, 2023
48a0fe4
Add section in module resolution
andrewbranch Apr 4, 2023
f709c11
Add section on declaration files
andrewbranch Apr 5, 2023
523ecff
WIP ESM/CJS interop
andrewbranch Apr 17, 2023
92c2876
Fix mermaid in GH’s renderer
andrewbranch Apr 17, 2023
e2b8ed9
Progress on esModuleInterop
andrewbranch Apr 20, 2023
6ee93d1
Progress on allowSyntheticDefaultImports
andrewbranch Apr 21, 2023
f57d860
Finish(?) esModuleInterop section
andrewbranch Apr 24, 2023
e76778b
WIP Node interop
andrewbranch Apr 25, 2023
950ab92
Finish enumerating differences between Node ESM and transpiled ESM
andrewbranch Apr 26, 2023
5b5b57b
Update outline
andrewbranch Apr 27, 2023
1765cb5
Wrap up argument for esModuleInterop
andrewbranch May 1, 2023
d9a4949
Finish esModuleInterop section
andrewbranch May 10, 2023
ceeadd2
Finish common resolution features
andrewbranch May 11, 2023
fd9236e
Section on allowImportingTsExtensions
andrewbranch May 12, 2023
03240af
Module resolution for libraries
andrewbranch May 15, 2023
14e863a
Rename file
andrewbranch Jun 7, 2023
1c9ac68
Choosing compiler options
andrewbranch Aug 1, 2023
c3f001f
Reference section on `module`
andrewbranch Aug 7, 2023
f0c32cc
WIP moduleResolution reference
andrewbranch Aug 10, 2023
ab6f9ae
Update modules/00_0_Outline.md
andrewbranch Aug 11, 2023
61dcb01
Add `"imports"` and self-name reference section
andrewbranch Aug 11, 2023
f827501
Add more imports/exports examples
andrewbranch Aug 14, 2023
b83f74e
Add typesVersions section
andrewbranch Aug 14, 2023
238bbad
Add example of exports blocking resolution
andrewbranch Aug 16, 2023
992f7a4
Directory modules, extensionless paths, package-relative paths, node1…
andrewbranch Aug 17, 2023
be68396
Update TODOs
andrewbranch Aug 22, 2023
3297cef
Update modules/01_Theory.md
andrewbranch Aug 22, 2023
077fd02
Paths WIP
andrewbranch Sep 7, 2023
b5dc67e
Finish paths
andrewbranch Sep 8, 2023
6dd61a3
Delete rootDirs section
andrewbranch Sep 8, 2023
2d011ef
bundler reference section
andrewbranch Sep 21, 2023
4f054b8
node10 reference section
andrewbranch Sep 21, 2023
cfedec8
Module syntax
andrewbranch Sep 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .vscode/md.css
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;
}
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"markdown.styles": [
".vscode/md.css"
]
}
68 changes: 68 additions & 0 deletions modules/00_0_Outline.md
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
8 changes: 8 additions & 0 deletions modules/00_1_Introduction.md
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.
409 changes: 409 additions & 0 deletions modules/01_Theory.md

Large diffs are not rendered by default.

175 changes: 175 additions & 0 deletions modules/02_01_Guides_Choosing_compiler_options.md
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`

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

}
}
```

### 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.
Loading