GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Type-only imports and exports by andrewbranch · Pull Request #35200 · microsoft/TypeScript

Conversation

@andrewbranch

@andrewbranch Andrew Branch (andrewbranch) commented Nov 19, 2019 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

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 in transpileOnly mode) sometimes have the opposite problem, where a re-export of a type should 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 type T, 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 in Prevent collision of imported type with exported declarations in current module #31231. Previously, you could replace export { JustAType } from './a' with

    import { JustAType } from './a';
    export type JustAType = JustAType;

    But as of TypeScript 3.7, we disallow the name collision of the locally declared JustAType with the imported name JustAType.

  2. If a Webpack user was left with an erroneous export { 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, Tobias Koppers (@sokra) has expressed some desire to make these warnings errors.

Proposal

  • Add type-only imports and exports similar to Implicit 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 as preserve but also adds an error whenever an import could be written as an import type

Syntax

Supported forms are:

import type T from './mod';
import type { A, B } from './mod';
import type * as Types from './mod';

export type { T };
export type { T } from './mod';

Possible additions but I think not terribly important:

export type * from './mod';
export type * as Types from './mod'; // pending #4813

We notably do not 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 of import type A, { B } from './mod' should be. Does type apply only to the default import A, 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., is only a type), name resolution for that symbol is unaffected. If the symbol does have a value side, name resolution for that symbol will see only the type side. The typical example is a class:

// @Filename: /a.ts
export default class A {}

// @Filename: /b.ts
import type A from './a';
new A();
//  ^ 'A' only refers to a type, but is being used as a value here.

function f(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.ts
namespace ns {
  export type Type = string;
  export class Class {}
  export const Value = "";
  export namespace nested {
    export class NestedClass {}
  }
}
export default ns;

// @Filename: /index.ts
import type ns from './ns';
const x = ns.Value;
//        ^^ Cannot use namespace 'ns' as a value.

let c: ns.nested.NestedClass;

Emit

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

// @importsNotUsedAsValue: preserve

// @Filename: /a.ts
import { T } from './mod';
let x: T;

// @Filename: /a.js
import "./mod";
let x;

Back-compat flag

There’s a new flag removeUnusedImports. 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:

  • Explicit keyword for type-only import #20433
  • importing type declaration files #16472
  • Request: compiler option to disable elision of "unused" imports #18586
  • Typescript should not be doing code removal for non TS code #30525
  • ES Module imports discarded when they include a type and a side-effect #28285

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

j-oliveras, sokra, dragomirtitian, mrmlnc, Jack-Works, oriSomething, klimashkin, vkrol, epaew, mjbvz, and 60 more reacted with thumbs up emoji j-oliveras, mrmlnc, klimashkin, swar30, vkrol, BPScott, AviVahl, ExE-Boss, miyaokamarina, OctoD, and 16 more reacted with hooray emoji ExE-Boss, wclr, OctoD, vkrol, ibezkrovnyi, proofit404, robpalme, carburo, MLoughry, elektronik2k5, and 14 more reacted with heart emoji j-oliveras, mrmlnc, klimashkin, vkrol, BPScott, ExE-Boss, r3nya, robpalme, amitbeck, unao, and 5 more reacted with rocket emoji
All reactions
  • 👍 70 reactions
  • 🎉 26 reactions
  • ❤️ 24 reactions
  • 🚀 15 reactions
@andrewbranch Andrew Branch (andrewbranch) mentioned this pull request Nov 20, 2019
getTypeAtLocation fails for import specifiers that resolve to a type alias #35233
Closed
andrewbranch
Andrew Branch (andrewbranch) commented Nov 20, 2019
View reviewed changes
Comment thread src/compiler/checker.ts Resolved

Uh oh!

There was an error while loading. Please reload this page.

@andrewbranch
Andrew Branch (andrewbranch) force-pushed the feature/type-only branch 2 times, most recently from f26046a to d5e3ebb Compare November 20, 2019 22:58
@ajafff

Klaus Meinhardt (ajafff) commented Nov 21, 2019

Copy link
Copy Markdown
Contributor

Andrew Branch (@andrewbranch) what about imported values that are only used for their types via typeof in the file? Are these imports still elided?

nyngwang reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@andrewbranch

Andrew Branch (andrewbranch) commented Nov 21, 2019 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member Author

Klaus Meinhardt (@ajafff) I think ideally the plan would be no, imports not marked with type are never elided. Flow has an import typeof form for this use case. I think that’s probably a reasonable follow-up feature. I had initially thought of import 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 the typeof a value, the original proposal doesn’t allow for that. /cc Daniel Rosenwasser (@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 is typeof import('./mod').SomeClass

Bnaya, bfricka, and ycycwx reacted with thumbs up emoji
All reactions
  • 👍 3 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@andrewbranch Andrew Branch (andrewbranch) added the Update Docs on Next Release Indicates that this PR affects docs label Nov 21, 2019
@andrewbranch
Andrew Branch (andrewbranch) requested review from Daniel Rosenwasser (DanielRosenwasser), Ryan Cavanaugh (RyanCavanaugh), Ron Buckton (rbuckton), Nathan Shively-Sanders (sandersn), Sheetal Nandi (sheetalkamat) and Wesley Wigham (weswigham) November 25, 2019 18:33
Andrew Branch (andrewbranch) added 17 commits November 25, 2019 10:41
@andrewbranch
Add type-only support for export declarations
426685a
@andrewbranch
Use a synthetic type alias instead of binding type-only exports as a …
856fe0f
…type alias
@andrewbranch
Works for re-exports!
9c659f6
@andrewbranch
isolatedModules works fine
a3d16f6
@andrewbranch
Diagnostic for type-only exporting a value
edfc798
@andrewbranch
Start isolated modules codefix
84238f9
@andrewbranch
Update for LKG control flow changes
5d2bc5d
@andrewbranch
Type-only import clause parsing
e736045
@andrewbranch
Type-only default import checking
558dd49
@andrewbranch
Type-only named imports
151a397
@andrewbranch
Fix isolated modules error
85390d7
@andrewbranch
Filter namespaces down to type-only
e130453
@andrewbranch
Fix class references
1db2773
@andrewbranch
Test nested namespaces
bd73f27
@andrewbranch
Test circular type-only imports/exports
fa72cf5
@andrewbranch
Fix getTypeAtLocation for type-only import/export specifiers
6de5150
@andrewbranch
Fix type-only generic imports
9bf28fe
@Rugvip Patrik Oldsberg (Rugvip) mentioned this pull request May 16, 2020
Add support for TS type-only re-exports alangpierce/sucrase#533
Merged
@sorenlouv Søren Louv-Jansen (sorenlouv) mentioned this pull request May 21, 2020
Prevents importing of public code into server elastic/kibana#67149
Merged
GNOME Infrastructure Automation Bot (gnomesysadmins) pushed a commit to GNOME/gtksourceview that referenced this pull request Jun 11, 2020
@jefferyto
typescript.lang: Add highlighting for type-only imports and exports
8677453
This syntax was added in TypeScript 3.8. The supported forms are from
the feature pull request[1].

[1]: microsoft/TypeScript#35200
@fwienber

Frank Wienberg (fwienber) commented Jun 17, 2020

Copy link
Copy Markdown

Maybe I'm missing something and I'm not sure where to post this remark, but should import type really make the importing ts 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: use import(<module>).default in the non-module everywhere 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 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.

crisperdue reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@andrewbranch

Andrew Branch (andrewbranch) commented Jun 17, 2020

Copy link
Copy Markdown
Member Author

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 add export {} if you don’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 you imported something, but then never used it in a value position, the output 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 file must be parsed as a module, and the output can be parsed as a module, so treating the file as a module seems like the best choice.

When you write an import type declaration, it’s essentially the same as writing an import declaration that you never use in an emitting position, except that it’s enforced that you never use it in an emitting position. Now, since import 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 that import type does not constitute a module. But ultimately, given how similar it is to writing a regular import 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 think import 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 emoji fwienber and gomain reacted with heart emoji
All reactions
  • 👍 10 reactions
  • ❤️ 2 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@johnnyreilly

John Reilly (johnnyreilly) commented Jun 17, 2020

Copy link
Copy Markdown

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

maxmilton and fwienber reacted with thumbs up emoji andrewbranch, DanielRosenwasser, and Blasz reacted with heart emoji
All reactions
  • 👍 2 reactions
  • ❤️ 3 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@fwienber

Frank Wienberg (fwienber) commented Jun 18, 2020

Copy link
Copy Markdown

Well, it was my question, and although the outcome is maybe not what I wanted 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
All reactions
  • ❤️ 5 reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@timonson Timo Rossa (timonson) mentioned this pull request Jul 6, 2020
Deno.bundle() should accept the compiler flag importsNotUsedAsValues denoland/deno#6663
Closed
@andrewbranch Andrew Branch (andrewbranch) mentioned this pull request Jul 14, 2020
Crash tsc --watch on adding "module": "none" to tsconfig #39597
Closed
@andrewbranch Andrew Branch (andrewbranch) mentioned this pull request Aug 3, 2020
Should I explicitly separate "import" and "import type" statements? #39861
Closed
@1000hz Cina Saffary (1000hz) mentioned this pull request Sep 16, 2020
type-only import prevents declaration of a value with the same name #40583
Closed
@evanw Evan Wallace (evanw) mentioned this pull request Oct 11, 2020
[typescript] Option to remove only type imports evanw/esbuild#448
Closed
@simllll Simon Tretter (simllll) mentioned this pull request Oct 15, 2020
Feature/switch to ts agenda/agenda#1129
Merged
@MartinJohns Martin Johns (MartinJohns) mentioned this pull request Nov 17, 2020
import type should not have any effect on output code #41562
Closed
@JamieB-gu Jamie B (JamieB-gu) mentioned this pull request Jan 25, 2021
Introduce decidePalette to DCR guardian/dotcom-rendering#2378
Merged
@TrevorKarjanis Trevor Karjanis (TrevorKarjanis) mentioned this pull request Feb 10, 2021
Enable TypeScript Rules (without requiring type checking rules) ni/javascript-styleguide#18
Merged
Trish Ta (vividviolet) added a commit to Shopify/ui-extensions that referenced this pull request Apr 19, 2021
@vividviolet
fix: clean up webpack configs and package.json
758eba0
- fixed react-i18n duplicated dependency
- removed all unused dependencies
- update webpack configs to use babel/preset-env and ensure the right plugins are loaded

chore: fix issue with babel warnings when re-exporting types

- refer to microsoft/TypeScript#35200 and babel/babel#10981
@antoinep92 Antoine Poliakov (antoinep92) mentioned this pull request Jun 11, 2021
import type should not need runtime modules support #44549
Closed
5 tasks done
@liby Bryan Lee (liby) mentioned this pull request Jul 20, 2021
chore(imports): update the imports declarations of type annotations and declarations to import type pmndrs/jotai#606
Merged
@mattrunyon Matthew Runyon (mattrunyon) mentioned this pull request Jul 22, 2021
Convert Linker to TypeScript deephaven/web-client-ui#123
Merged
@flaviendelangle Flavien DELANGLE (flaviendelangle) mentioned this pull request Jul 26, 2021
[typescript] Improve type coverage of colDef mui/mui-x#2188
Merged
@jablko Jack Bates (jablko) mentioned this pull request Oct 27, 2021
Allow inlined-value usage of type-only const enums #46556
Closed
@github-actions github-actions Bot mentioned this pull request Aug 14, 2022
Remove span tag in title and unnecessary TOC in Ko microsoft/TypeScript-Website-Localizations#164
Merged
@airman5573 byeongin yoon (airman5573) mentioned this pull request Sep 7, 2022
[성능 베이스캠프 미션] 병민(윤병인) 미션 제출합니다. woowacourse/perf-basecamp#39
Merged
9 tasks done
@andrewbranch Andrew Branch (andrewbranch) mentioned this pull request Nov 10, 2022
Proposal: deprecate importsNotUsedAsValues and preserveValueImports in favor of single flag #51479
Closed
@privatenumber Hiroki Osame (privatenumber) mentioned this pull request Sep 27, 2023
importing and exporting an interface fails for type module in package.json and ESXXXX module in tsconfig.json privatenumber/tsx#272
Closed
1 task
@nyngwang

nyngwang commented Mar 12, 2024

Copy link
Copy Markdown

preserve keeps imports 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 issue Implicit module import/export elision #2812, and by their "Proposed change 1. Do not elide module imports", I thought the word "imports" referred to individual names imported, e.g. AType in import { 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 is importsNotUsedAsValues: preserve is not preserving #43393. It took me some time to realize that "imports" means every import-statement for names without value-side usage. (and I indeed found the naming importsNotUsedAsValues precise and clean)

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

All in all, I like TypeScript :)

andrewbranch reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@Gururajj77 Gururaj J (Gururajj77) mentioned this pull request Jul 25, 2024
[Feature Request]: As a Developer, I would like to import types from @carbon/react carbon-design-system/carbon#16436
Closed
1 task done
@RyanCavanaugh Ryan Cavanaugh (RyanCavanaugh) mentioned this pull request Jul 7, 2025
Feature request: Allow to combine default type import with named imports #61972
Closed
6 tasks done
@microsoft Microsoft (microsoft) locked as resolved and limited conversation to collaborators Oct 16, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Reviewers

@sandersn Nathan Shively-Sanders (sandersn) Awaiting requested review from sandersn
@sheetalkamat Sheetal Nandi (sheetalkamat) Awaiting requested review from sheetalkamat
@DanielRosenwasser Daniel Rosenwasser (DanielRosenwasser) Awaiting requested review from DanielRosenwasser
@rbuckton Ron Buckton (rbuckton) Awaiting requested review from rbuckton
@RyanCavanaugh Ryan Cavanaugh (RyanCavanaugh) Awaiting requested review from RyanCavanaugh
@weswigham Wesley Wigham (weswigham) Awaiting requested review from weswigham

Uh oh!

There was an error while loading. Please reload this page.

Assignees

@andrewbranch Andrew Branch (andrewbranch)

Labels

Author: Team Update Docs on Next Release Indicates that this PR affects docs

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Add syntax hint to allow reexports of types in isolatedModules mode Should be possible to force import of "unused" modules in case there are side-effects

Uh oh!

There was an error while loading. Please reload this page.

11 participants

@andrewbranch @ajafff @typescript-bot @DanielRosenwasser @johnnyreilly @nicolo-ribaudo @stevefan1999-personal @elektronik2k5 @ExE-Boss @fwienber @nyngwang
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.