Type-only import specifiers by andrewbranch · Pull Request #45998 · microsoft/TypeScript (original) (raw)
Follow-up feature to #44619, allows
import { SomeComponent, type SomeProps } from "./component"
Emit behavior
- Adding the
type
keyword before an import or export specifier causes it to be elided from JS emit. - The
type
keyword on an import or export specifier plays no direct role in whether the parent import/export declaration is elided.
This means that under default settings,
import { type SomeProps } from "./component";
gets erased from the JS entirely, because default settings remove any import declarations that can be removed. But under --importsNotUsedAsValues=preserve
(or error
), the same code is emitted as
import {} from "./component";
because the type
keyword on the imports specifier does not indicate that there is no runtime module dependency on "./component"
.
Auto-imports behavior
As discussed in #44619, the combination of --isolatedModules
and --preserveValueImports
requires that exported symbols that have no value meaning (i.e., they’re purely types or uninstantiated namespaces) or are imported/exported with a type-only import/export somewhere earlier in the chain be imported with a type-only import so transpilers can know to drop those import specifiers. Obviously, auto-imports had to be updated to account for this when importing types or already-type-only symbols. This PR includes the changes to make working under those settings manageable, though there are further experience enhancements I need to investigate. Here’s a non-exhaustive summary of what happens now:
- Barring bugs in the implementation, auto-imports should never trigger an error provided that the location you’re using the identifier to import was a valid place to use it (i.e., you can possibly mess it up by trying to import a type-only-exported class in an emitting position, but this is a rare edge case), so importing a type under these settings will always place the type in a type-only import.
- Auto-imports still generally prefers not to use type-only imports, so if you’re not using these settings or
--importsNotUsedAsValues=error
, you won’t see any type-only auto-imports. You’ll only get a type-only auto-import if it is needed to prevent an error or if it can use an existing type-only import declaration as opposed to adding a new import declaration. - When adding a type-only import in a new import declaration (as opposed to adding a specifier to an existing declaration), it prefers the top-level type-only declaration form
import type { Foo }
overimport { type Foo }
. - A top-level type-only import declaration will be converted to a regular import upon importing a value from the same module (
import type { SomeType }
→import { SomeValue, type SomeType }
) - There are exceptions to that rule due to the fact that default imports can only be type-only via a top-level type-only import declaration and a type-only import declaration can't have both a default and named bindings (
import type Foo, { Bar }
is not allowed).
Future improvements to look into:
- If you have enough specifiers in
import type { A, B, C, D, ... }
, it is probably undesirable to convert that to a regular import upon adding a value import to it, since it would result in an explosion oftype
prefixes. At some point, you probably just want to add a new import statement. On the other hand, if each specifier is on its own line, it might not be bad to add all the prefixes and keep them combined, so this scenario needs some thought in order for us to guess the best thing to do. - There’s still no support for promoting a type-only import to a regular import by attempting to use a value that you’ve already imported as type-only in an emitting position, e.g.
import type { SomeClass } from "./whatever";
const x = new SomeClass| // <- what to do here?
I used to think this was kind of sketchy, but now that auto-imports has to be willing to make a lot more guesses and be willing to put things in type-only imports more often, I’m coming around to the thought that the best thing we can do is just try to be as flexible as possible and fluidly rewrite your import format to match your usage as you go. (Existing import type prevents autocompletion of value import for TypeScript #44804 is related.)
Other follow-up work
- Update the TmGrammar for syntax highlighting