- Notifications
You must be signed in to change notification settings - Fork13k
Description
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';classMyComponent{constructor(privatemyService: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 topreserve
, 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, likeeval
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 asimport "./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-onlywas 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 withisolatedModules
) 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 byimportsNotUsedAsValues
andpreserveValueImports
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#51000, I propose deprecatingimportsNotUsedAsValues
andpreserveValueImports
, and replacing them with a single flag called (bikesheddable)verbatimModuleSyntax
. The effect ofverbatimModuleSyntax
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 withouttype
This is a stricter setting than eitherimportsNotUsedAsValues
orpreserveValueImports
(though it’s approximately what you get by combining both withisolatedModules
), because it requires that all types be marked as type-only. For example:
import{writeFile,WriteFileOptions}from"fs";
would be an error in--verbatimModuleSyntax
becauseWriteFileOptions
is only a type, so would be a runtime error if emitted to JS. This import would have to be written
import{writeFile,typeWriteFileOptions}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
. (Innode16
andnodenext
, 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
importfs= 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 arequire
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 outrequire
helps you understand when and why you can’trequire
an ES module—it’s easier to lose track of this when yourrequire
is disguised as an ESMimport
in the source file.