Proposal: Encode and read emit-affecting compiler options in declaration files #54212
Labels
Domain: Declaration Emit
The issue relates to the emission of d.ts files
In Discussion
Not yet reached consensus
Suggestion
An idea for TypeScript
TL;DR
Put emit-affecting options at the top of
.d.ts
files likeso we can know what’s going on in JS files when we read in
.d.ts
files from external dependencies.Super basic background
Type checking is designed to reason about what your JS code will do at runtime. When you change a compiler option that affects what JS code gets emitted, there is often a corresponding change to type checking behavior. For example, when emitting CommonJS modules, the
esModuleInterop
changes the JS emit for default imports so they can be used in more situations. Because of this emit change, a compiler error can go away:Here, the compiler knows that the JS generated for the default import will not work with the
"express"
module. But if we enableesModuleInterop
, the generated JS changes in a way that makes it compatible, and consequently the type checking error goes away. The presence or absence of the error is driven by accurate knowledge of the runtime JS.The problem
When a project consumes a type declaration file, say from an external dependency, the declaration file is supposed to contain everything the compiler needs to know about the JS file it represents—no more and no less. But declaration files typically don’t change in any way as a result of modifying emit-affecting settings. The compiler makes assumptions about the contents of the corresponding JS files based on the compiler options of whatever project consumes the declaration files. Since type checking behavior is driven by knowledge about the runtime behavior of JS files, type checking can be incorrect when consuming libraries compiled with emit-affecting options different from the consuming project’s options.
Returning to the
esModuleInterop
example, this issue has led to conventional wisdom like “libraries should not useesModuleInterop
.” The idea is that if a library usesesModuleInterop
to silence an error on a default import like the one shown above, then publishes type declaration files containing that default import, then every user who consumes those declarations must also enableesModuleInterop
. This conventional wisdom is probably the best we can do currently, but it’s also deeply flawed. Compiling withoutesModuleInterop
also allows libraries to write (categorically incorrect!) code that produces errors when compiled withesModuleInterop
:The issue is bidirectional; neither value of
esModuleInterop
guarantees that consumers will not have to adopt the setting used by the library.The problem is not unique to
esModuleInterop
; it applies to nearly all emit-affecting compiler options that result in a corresponding checking change. Other particularly relevant ones that come to mind are:useDefineForClassFields
preserveConstEnums
module
Other emit-affecting options are either not semantically meaningful (
removeComments
) or tend to be relevant only in implementation files (downlevelIteration
,jsxImportSource
).Proposal
I suggest encoding these relevant options into declaration files when emitting them, and reading and respecting them during checking. I’m not particular about syntax, but it needs to be human-writable so it can be used on DefinitelyTyped. Both the parsing and likely much of the checking infrastructure could be reused from #49886, so I would advocate for reusing that syntax:
// @esModuleInterop: true
These directives would be unconditionally added to the top of all emitted declaration files. Any directive-controllable options that are not set with a directive should fall back to today’s behavior (use the consuming project’s options). Note that this means we cannot assume a default value for directives and elide ones that would be redundant with that default, since no directive means “use the user’s config,” which could have a value that disagrees with whatever default we set. So all options we think are relevant should always be encoded in declaration file emit.
What about project references?
Project references on the command line consume declaration files, while in TS Server, they use the referenced project’s source
.ts
files. The checker may need some changes to ensure that the value of directive-controllable options are read from the referenced project’s tsconfig.People hand-writing definitions will mess it up!
The potential for messing up hand-written declarations is already so high that adding one more degree of freedom doesn’t move the needle at all. Careful checks will need to be implemented on DefinitelyTyped. Everyone else is on their own. I believe this will solve more problems than it creates.
Does this conflict with #49886?
I don’t think so. I suggest making such directives errors in implementation files, which may help preserve that space for #49886 if we find a way to make it viable performance-wise. Currently, none of the options I identified as interesting are targeted in #49886. If one of them was able to be overridden in an implementation file in the future, the directive would just have to be emitted as-is to the declaration file.
Will this affect performance?
I hope that the fact that this targets much fewer options than #49886, and ones that are checked somewhat less often than strictness flags, means the performance penalty will be negligible. But experimentation will be needed.
Alternatives
You can imagine a lot of ways to put something in a declaration file to encode extra information about imports, class fields, const enums, and others. I’m open to discussing very different syntax and potentially tackling a narrower scope than what I’ve discussed (I’m mostly invested in fixing
esModuleInterop
at the moment), but I can’t think of any way to do it without changing declaration file emit in some way. I think a file-level directive has merit because I think it can leverage existing work from #49886, and the settings cannot be changed more granularly than file-level anyway.Related: #52779, some const enum ones, surely others
The text was updated successfully, but these errors were encountered: