Proposal: deprecate `importsNotUsedAsValues` and `preserveValueImports` in favor of single flag
See original GitHub issueBackground: importsNotUsedAsValues
importsNotUsedAsValues
was introduced alongside type-only imports in https://github.com/microsoft/TypeScript/pull/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 https://github.com/microsoft/TypeScript/pull/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
- easier to explain
- less complex to implement
- well-suited to users who want to use type-only imports for stylistic reasons
On the schedule of https://github.com/microsoft/TypeScript/issues/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.
Issue Analytics
- State:
- Created 10 months ago
- Reactions:8
- Comments:8 (6 by maintainers)
We agreed to move forward with this.
verbatimModuleSyntax
, just because it’s more understandable thanisolatedModules
. It will imply/supersedeisolatesModules
.--module commonjs
(I think @jakebailey has investigated this a bit, but I don’t recall the conclusion)import type
but are not (the functionality we lose from--importsNotUsedAsValues error
)Yeah, I think that’s reasonable.