Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Type-only imports and exports#35200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged

Conversation

@andrewbranch
Copy link
Member

@andrewbranchandrewbranch commentedNov 19, 2019
edited
Loading

TL;DR:

  • import type { A } from './mod',export type { A } from './mod'
  • Add flag to stop eliding import declarations from emit

To do:

  • parsing
  • checking: default import
  • checking: named imports
  • checking: named exports
  • checking: namespace import
  • update isolatedModules error message
  • test with enums
  • isolatedModules code fix
  • auto imports behavior
  • compiler flag, emit behavior
  • checker API (getTypeAtLocation)
  • grammar error for mixing default and named bindings
  • code fix for splitting a default + named bindings into two import declarations
  • error for usage in JS
  • check/improve parsing diagnostics for common mistakes
  • test more quick info, completions, rename, etc.
  • TextMate grammar
  • auto-import/codefix for name in value space that’s already type-only imported?
  • code fix for--importsNotUsedAsValue=error error

Background

TypeScript elides import declarations from emit where, in the source, an import clause exists but all imports are used only in a type position[playground]. This sometimes creates confusion and frustration for users who write side-effects into their modules, as the side effects won’t be run if other modules import only types from the side-effect-containing module (#9191).

At the same time, users who transpile their code file by file (as in Babel, ts-loader intranspileOnly mode) sometimes have theopposite problem, where a re-export of a typeshould be elided, but the compiler can’t tell that the re-export is only a type during single-file transpilation (#34750,TypeStrong/ts-loader#751)[playground].

Prior art

In early 2015,Flow introduced type-only imports which would not be emitted to JS. (Their default behavior, in contrast to TypeScript’s, was never to elide imports, so type-only imports for them were intended to help users cut down on bundle size by removing unused imports at runtime.)

Two months later,#2812 proposed a similar syntax and similar emit behavior for TypeScript: the compiler would stop eliding import declarations from emit unless those imports were explicitly marked as type-only. This would give users who needed their imports preserved for side effects exactly what they wanted, and also give single-file transpilation users a syntactic hint to indicate that a re-export was type-only and could be elided:export type { T } from './mod' would re-export the typeT, but have no effect on the JavaScript emit.

#2812 was ultimately declined in favor of introducing the--isolatedModules flag, under which re-exporting a type is an error, allowing single-file transpilation users to catch ambiguities at compile time and write them a different way.

Since then

Over the last four years after#2812 was declined, TypeScript users wanting side effects have been consistently confused and/or frustrated. They have workarounds (read#9191 in full for tons of background and discussion), but they’re unappealing to most people.

For single-file transpilation users, though, two recent events have made their lives harder:

  1. In TypeScript 3.7, we sort of took away--isolatedModules users’ best workaround for reexporting a type inPrevent collision of imported type with exported declarations in current module #31231. Previously, you could replaceexport { JustAType } from './a' with

    import{JustAType}from'./a';exporttypeJustAType=JustAType;

    But as of TypeScript 3.7, we disallow the name collision of the locally declaredJustAType with the imported nameJustAType.

  2. If a Webpack user was left with an erroneousexport { JustAType } from './a' in their output JavaScript, Webpack 4 would warn, but compilation would succeed. Many users simply ignored this warning (or even filtered it out of Webpack’s output). But in Webpack 5 beta,@sokra has expressed some desire tomake these warningserrors.

Proposal

  • Add type-only imports and exports similar toImplicit module import/export elision #2812 and Flow
  • Change the default emit behavior of the compiler to stop eliding regular imports even if the imported names are only used in type positions
  • Always elide imports and re-exports explicitly marked as type-only
  • Add a (temporary?) compiler flag that restores the current behavior of eliding imports that are used only for types to help users with back-compat
  • Updated: Leave the default emit as-is, adding the flag--importsNotUsedAsValue <"remove" | "preserve" | "error"> to control the behavior
    • remove is default; maintains today’s behavior
    • preserve keeps imports used only for types in the emit as a side-effect import
    • error acts aspreserve but also adds an error whenever animport could be written as animport type

Syntax

Supported forms are:

importtypeTfrom'./mod';importtype{A,B}from'./mod';importtype*asTypesfrom'./mod';exporttype{T};exporttype{T}from'./mod';

Possible additions but I think not terribly important:

exporttype*from'./mod';exporttype*asTypesfrom'./mod';// pending #4813

We notably donot plan to support at this time:

  • type modifier on import/export specifiers:import { type A } from './mod',export { A, type B }
  • Mixing a type-only default import with named or namespace imports:import type T, { A } from './mod',import type T, * as ns from './mod'

The forms in the former bullet will be syntax errors; the forms in the latter will be grammar errors. We want to start with productions that can be read unambiguously, and it’s not immediately clear (especially in the absence of Flow’s implementation), what the semantics ofimport type A, { B } from './mod' should be. Doestype apply only to the default importA, or to the whole import clause? We prefer no one need wonder.

Type semantics

Any symbol with a type side may be imported or exported as type-only. If that symbol has no value side (i.e., isonly a type), name resolution for that symbol is unaffected. If the symboldoes have a value side, name resolution for that symbol will seeonly the type side. The typical example is a class:

//@Filename: /a.tsexportdefaultclassA{}//@Filename: /b.tsimporttypeAfrom'./a';newA();//  ^ 'A' only refers to a type, but is being used as a value here.functionf(obj:A){}// ok

If the symbol is a namespace, resolution will see a mirror of that namespace recursively filtered down to just its types and namespaces:

//@Filename: /ns.tsnamespacens{exporttypeType=string;exportclassClass{}exportconstValue="";exportnamespacenested{exportclassNestedClass{}}}exportdefaultns;//@Filename: /index.tsimporttypensfrom'./ns';constx=ns.Value;//        ^^ Cannot use namespace 'ns' as a value.letc:ns.nested.NestedClass;

Emit

Updated: When theimportsNotUsedAsValue flag is set to 'preserve', type-only import declarations will be elided. Regular imports where all imports are unused or used only for types willnot be elided (only the import clause will be elided):

//@importsNotUsedAsValue: preserve//@Filename: /a.tsimport{T}from'./mod';letx:T;//@Filename: /a.jsimport"./mod";letx;

Back-compatflag

There’s a new flagremoveUnusedImports. Its name is not perfect because it really means “remove imports that have imported names that never get used in a value position.” Open to suggestions.

Updated: this PR is backward-compatible by default.

Auto-imports behavior

  • Symbols without a value side will be imported as type-only if there’s not already a regular import from the containing module. If there’s an existing import from the containing module, it will be added to that import (as happens today).
  • There will be a configuration option to disable type-only auto imports entirely (since some people use VS Code’s TypeScript version for editor services but compile with an older version).

I’m not yet confident what other changes, if any, will the right move, but the main scenarios to consider are:

  • User auto imports a class, enum, or namespace in a type position. Should we do a type-only import?
  • User has a type-only import of a class, enum, or namespace, then later tries to use the same symbol in a value position. Do we convert the type-only import to a regular import? (Is that even possible with a completions code action?)

Successor of#2812
Closes#9191
Closes#34750

Would close if they were still open:

j-oliveras, sokra, dragomirtitian, mrmlnc, Jack-Works, oriSomething, klimashkin, vkrol, epaew, mjbvz, and 60 more reacted with thumbs up emojij-oliveras, mrmlnc, klimashkin, swar30, vkrol, BPScott, AviVahl, ExE-Boss, miyaokamarina, OctoD, and 16 more reacted with hooray emojiExE-Boss, wclr, OctoD, vkrol, ibezkrovnyi, proofit404, robpalme, carburo, MLoughry, elektronik2k5, and 14 more reacted with heart emojij-oliveras, mrmlnc, klimashkin, vkrol, BPScott, ExE-Boss, r3nya, robpalme, amitbeck, unao, and 5 more reacted with rocket emoji
@andrewbranchandrewbranchforce-pushed thefeature/type-only branch 2 times, most recently fromf26046a tod5e3ebbCompareNovember 20, 2019 22:58
@ajafff
Copy link
Contributor

@andrewbranch what about imported values that are only used for their types viatypeof in the file? Are these imports still elided?

nyngwang reacted with thumbs up emoji

@andrewbranch
Copy link
MemberAuthor

andrewbranch commentedNov 21, 2019
edited
Loading

@ajafff I think ideally the plan would be no, imports not marked withtype are never elided. Flow has animport typeof form for this use case. I think that’s probably a reasonable follow-up feature. I had initially thought ofimport typeof as syntactic sugar for something already possible, but as you bring up, if you care about eliding imports that are unnecessary at runtime but you need thetypeof a value, the original proposal doesn’t allow for that. /cc@DanielRosenwasser thoughts?

Of course, a workaround is to export a type alias from the file where the value was exported and import that instead, but you can’t do that if the value in question comes from a third party library.


Edit: a surefire workaround istypeof import('./mod').SomeClass

Bnaya, bfricka, and ycycwx reacted with thumbs up emoji

gnomesysadmins pushed a commit to GNOME/gtksourceview that referenced this pull requestJun 11, 2020
This syntax was added in TypeScript 3.8. The supported forms are fromthe feature pull request[1].[1]:microsoft/TypeScript#35200
@fwienber
Copy link

Maybe I'm missing something and I'm not sure where to post this remark, but shouldimport type really make the importingts file a module?
My use case: I implement a class in TypeScript as a module, but want to use its interface in a non-module script. Currently, I have to extract the interface into a non-module, and all its transitive dependencies, too, otherwise my non-module becomes a module just because it's importing a type.
Alternative workaround: useimport(<module>).default in the non-moduleeverywhere the type is used (I cannot use a local type declaration in global scope, as that would be visible everywhere).
I thought the definition of when ajs file becomes a module is whenever it imports or exports anything. Type-only import/export, which is eliminated in JavaScript output, should not influence that behavior.

crisperdue reacted with thumbs up emoji

@andrewbranch
Copy link
MemberAuthor

I think that’s a reasonable question, and I did think about it while writing this feature. I’ll try to explain my thought process.

I thought the definition of when a js file becomes a module is whenever it imports or exports anything. Type-only import/export, which is eliminated in JavaScript output, should not influence that behavior.

So first, just to clear up the background and definitions—it sounds like you probably know this, but just making sure the grounding for the rest of my argument is established. When we’re talking about JavaScript you’re correct: it is true that an import or export makes a file a module. However, the converse is not true. A file without an import or export is both a valid module and a valid script. But for the purposes of type checking, we have to make a decision about how to treat every file. So if we don’t see any imports or exports, we unequivocally treat the file as a script—it’s easy and harmless to addexport {} if youdon’t want the file to be a script.

Now, consider that we’ve always elided unused imports from our JS emit. (As of this PR, that’s configurable, but the default is still to elide unused imports.) If you write a TypeScript file that looks obviously module-like because youimported something, but then never used it in a value position, theoutput JS will look like a valid script. Does that mean we should treat the TypeScript file as a script? I don’t think so. The source filemust be parsed as a module, and the outputcan be parsed as a module, so treating the file as a module seems like the best choice.

When you write animport type declaration, it’s essentially the same as writing animport declaration that you never use in an emitting position, except that it’senforced that you never use it in an emitting position. Now, sinceimport type is not standard ES grammar, I think you could reasonably argue that we could implement whatever rules we wanted; that it would not be inherently incorrect to say thatimport type does not constitute a module. But ultimately, given how similar it is to writing a regularimport of a type, I think maintaining consistency that all import declarations are module markers was the correct choice.

That said, I 100% agree with you that referencing modules in scripts is painful—I’ve hit that exact problem before. But I don’t thinkimport type would have been the best solution, even if we hadn’t already settled on the current behavior.

johnnyreilly, vkrol, maxmilton, ExE-Boss, fwienber, s4m0r4m4, Blasz, elmpp, yongholeeme, and nyngwang reacted with thumbs up emojifwienber and gomain reacted with heart emoji

@johnnyreilly
Copy link

This wasn't my question, but I just wanted to give props to@andrewbranch for such a thoughtful and clear answer. Well done sir!

maxmilton and fwienber reacted with thumbs up emojiandrewbranch, DanielRosenwasser, and Blasz reacted with heart emoji

@fwienber
Copy link

Well, it wasmy question, and although the outcome is maybe not what Iwanted to hear, I must second that this is a very thoughtful and clear answer and I agree 100%. Thank you, much appreciated!

DanielRosenwasser, andrewbranch, johnnyreilly, s4m0r4m4, and Blasz reacted with heart emoji

vividviolet added a commit to Shopify/ui-extensions that referenced this pull requestApr 19, 2021
- fixed react-i18n duplicated dependency- removed all unused dependencies- update webpack configs to use babel/preset-env and ensure the right plugins are loadedchore: fix issue with babel warnings when re-exporting types- refer tomicrosoft/TypeScript#35200 andbabel/babel#10981
@nyngwang
Copy link

preserve keepsimports used only for types in the emit as a side-effect import

While it's a bit late, I still want to point out that the wording is a bit confusing.

  1. I first read the source issueImplicit module import/export elision #2812, and by their "Proposed change 1. Do not elide moduleimports", I thought the word "imports" referred to individualnames imported, e.g.AType inimport { AType }, as the example code they provided.
  2. This unfortunately caused a problem when I tried to understand the bolded "imports" quoted above. I believe it's not just me, as there isimportsNotUsedAsValues: preserve is not preserving #43393. It took me some time to realize that "imports" means everyimport-statement for names without value-side usage. (and I indeed found the namingimportsNotUsedAsValues precise and clean)

I hope this could help someone like me who is interested in the history of deprecated options covered by the newverbatimModuleSyntax option in the future.

All in all, I like TypeScript :)

andrewbranch reacted with thumbs up emoji

Sign up for freeto subscribe to this conversation on GitHub. Already have an account?Sign in.

Reviewers

@sandersnsandersnAwaiting requested review from sandersn

@sheetalkamatsheetalkamatAwaiting requested review from sheetalkamat

@DanielRosenwasserDanielRosenwasserAwaiting requested review from DanielRosenwasser

@rbucktonrbucktonAwaiting requested review from rbuckton

@RyanCavanaughRyanCavanaughAwaiting requested review from RyanCavanaugh

@weswighamweswighamAwaiting requested review from weswigham

Assignees

@andrewbranchandrewbranch

Labels

Author: TeamUpdate Docs on Next ReleaseIndicates that this PR affects docs

Projects

None yet

Milestone

No milestone

11 participants

@andrewbranch@ajafff@typescript-bot@DanielRosenwasser@johnnyreilly@nicolo-ribaudo@stevefan1999-personal@elektronik2k5@ExE-Boss@fwienber@nyngwang

[8]ページ先頭

©2009-2025 Movatter.jp