Proposal: deprecate importsNotUsedAsValues and preserveValueImports in favor of single flag · Issue #51479 · microsoft/TypeScript (original) (raw)

Background: importsNotUsedAsValues

importsNotUsedAsValues was introduced alongside type-only imports in #35200 as a way to control import elision. In particular, Angular users often experienced runtime errors due to the unintended import elision in files like:

import { MyService } from './MyService';

class MyComponent { constructor(private myService: MyService) { } }

It appears to TypeScript as if the import declaration can be elided from the JS emit, but the ./MyService module contained order-sensitive side effects. By setting the new --importsNotUsedAsValues flag to preserve, import declarations would not be elided, and the module loading order and side effects could be preserved. Type-only imports could then be used to elide specific import declarations.

Background: preserveValueImports

preserveValueImports was added in #44619 as a way to control elision of individual imported names so that symbols can be referenced from places TypeScript cannot analyze, like eval statements or Vue templates:

import { doSomething } from "./module";

eval("doSomething()");

Under default compiler options, the entire import statement is removed, so the eval’d code fails. Under --importsNotUsedAsValues preserve, the import declaration is preserved as import "./module" since the flag is only concerned with module loading order and potential side effects that may be contained in "./module". Under the new --preserveValueImports option, doSomething would be preserved even though the compiler thinks it is unused.

In the same release, the ability to mark individual import specifiers as type-only was added as a complement to --preserveValueImports.

User feedback

These two flags, along with type-only import syntax, were designed to solve fairly niche problems. Early on, I encouraged users not to use type-only imports unless they were facing one of those problems. But as soon as they were available, and consistently since then, we have seen enthusiasm for adopting type-only imports everywhere possible as an explicit marker of what imports will survive compilation to JS. But since the flags were not designed to support that kind of usage of type-only imports, the enthusiasm has been accompanied by confusion around the configuration space and frustration that auto-imports, error checking, and emit don’t align with users’ mental model of type-only imports.

Further, because the two flags were designed at different times to address different issues, they interact with each other (and with isolatedModules) in ways that are difficult to explain without diving into the background of each flag and the narrow problems they were intended to solve. And the flag names do nothing to clear up this confusion.

Proposal

We can solve the problems addressed by importsNotUsedAsValues and preserveValueImports with a single flag that is

On the schedule of #51000, I propose deprecating importsNotUsedAsValues and preserveValueImports, and replacing them with a single flag called (bikesheddable) verbatimModuleSyntax. The effect of verbatimModuleSyntax can be described very simply:

verbatimModuleSyntax: Emits imports and exports to JS outputs exactly as written in input files, minus anything marked as type-only. Includes checks to ensure the resulting output will be valid.

No elision without type

This is a stricter setting than either importsNotUsedAsValues or preserveValueImports (though it’s approximately what you get by combining both with isolatedModules), because it requires that all types be marked as type-only. For example:

import { writeFile, WriteFileOptions } from "fs";

would be an error in --verbatimModuleSyntax because WriteFileOptions is only a type, so would be a runtime error if emitted to JS. This import would have to be written

import { writeFile, type WriteFileOptions } from "fs";

No transformations between module systems

True to its name, verbatimModuleSyntax has another consequence: ESM syntax cannot be used in files that will emit CommonJS syntax. For example:

import { writeFile } from "fs";

This import is legal under --module esnext, but an error in --module commonjs. (In node16 and nodenext, it depends on the file extension and/or the package.json "type" field.) If the file is determined to be a CommonJS module at emit by any of these settings, it must be written as

import fs = require("fs");

instead. Many users have the impression that this syntax is legacy or deprecated, but that’s not the case. It accurately reflects that the output will use a require statement, instead of obscuring the output behind layers of transformations and interop helpers. I think using this syntax is particularly valuable in .cts files under --module nodenext, because in Node’s module system, imports and requires have markedly different semantics, and actually writing out require helps you understand when and why you can’t require an ES module—it’s easier to lose track of this when your require is disguised as an ESM import in the source file.