Repository: TypeScript
Issue: Tracking issue: Named imports from CJS module incorrectly allowed in nodenext
Labels: Discussion

This issue has come up a few times, most recently at https://github.com/DefinitelyTyped/DefinitelyTyped/pull/65147#issuecomment-1515134407, and I don’t think we’ve had a canonical place to explain, discuss, or track it.

## The symptom

Sometimes, when you’re writing an ES module in `--module nodenext`, you will want to import a CJS dependency, and TypeScript lets you used a named import:

```ts
import { createProgram } from "typescript";
createProgram(/* ... */);
```

but Node complains:

```
file:///project/out/main.mjs:1
import { createProgram } from "typescript";
         ^^^^^^^^^^^^^
SyntaxError: Named export 'createProgram' not found. The requested module 'typescript' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'typescript';
const { createProgram } = pkg;
```

Alternatively but equivalently, you might try to use a namespace import:

```ts
import * as ts from "typescript";
ts.createProgram(/* ... */);
```

and Node gives a much less helpful error:

```
file:///project/out/main.mjs:2
ts.createProgram();
   ^

TypeError: ts.createProgram is not a function
```

In either case, you find that you instead need to write a default import:

```ts
import ts from "typescript";
ts.createProgram(/* ... */);
```

Both TypeScript and Node are happy with this, but why did TypeScript let you write the forms that crash at runtime?

## The problem

Node uses [cjs-module-lexer](https://github.com/nodejs/cjs-module-lexer) to syntactically analyze CommonJS modules without executing them in order to turn `module.exports` properties into named exports that can be imported as named (or namespace) imports by ES modules. However, syntactic analysis has its limitations, and not all `module.exports` properties get detected on all modules. (In practice, it’s common for this to be all-or-nothing, as with the `typescript` package in its current state: it has _no_ named exports available.)

Type definitions have no way of declaring which exports/properties of a CommonJS module will be detectable by Node’s named export analysis. In other words, `typescript.d.ts` is not misrepresenting the shape of the module; it would be impossible to “fix” it for Node ESM consumers without breaking it for CJS consumers or bundlers, which typically have more relaxed interop rules. TypeScript currently assumes that _all_ declared exports/properties of a CJS module will be detectable by Node and available as named imports. This is unsound, leading to the error in the example above.

It should be noted that at least for the _named_ import case, the crash occurs during Node’s module linking phase (as soon as the module graph is loaded—at startup, unless the affected part of the graph is isolated in a dynamic import), with a good runtime error message. While annoying to run into, it’s usually immediately diagnosable and fixable. The namespace import variation is a bit more insidious, since the import can be linked, but subsequent property accesses, which can occur later in execution, may fail.

## Solutions and non-solutions

I’m putting this issue up for tracking/documentation purposes, not to advocate for a fix. But it’s worth mentioning a few ways of addressing the problem since people will ask or suggest them:

- We could make a flag that makes potentially unsafe named/namespace imports always error. It would be very cumbersome and catch a lot of false positives, but some people may prefer the safety over the convenience.
- We could make it possible to annotate modules or individual exports/properties in type declarations that cannot be correctly analyzed by Node. This sounds pretty fraught and fragile to me, given how often people postprocess both their types and JS by third-party build tools.
- We _cannot_ reasonably determine which exports/properties are detectable by Node by looking at the JS, e.g. by running cjs-module-lexer ourselves, because we never even resolve or read JS sources when types are found for them. Even without incurring the cost of running the (fast) lexer, the performance penalty for doing twice as much module resolution and file system hits would be unacceptable.