|
| 1 | +--- |
| 2 | +authors: |
| 3 | + - image_url: https://www.joshuakgoldberg.com/img/josh.jpg |
| 4 | + name: Josh Goldberg |
| 5 | + title: typescript-eslint Maintainer |
| 6 | + url: https://github.com/JoshuaKGoldberg |
| 7 | +description: Why enforcing TypeScript imports use the `type` modifier when possible benefits some project setups. |
| 8 | +slug: consistent-type-imports-and-exports-why-and-how |
| 9 | +tags: [typescript, imports, exports, types, transpiling] |
| 10 | +title: 'Consistent Type Imports and Exports: Why and How' |
| 11 | +--- |
| 12 | + |
| 13 | +`import` and `export` statements are core features of the JavaScript language. |
| 14 | +They were added as part of the [ECMAScript Modules (ESM)](https://nodejs.org/api/esm.html#modules-ecmascript-modules) specification, and now are generally available in most mainstream JavaScript environments, including all evergreen browsers and Node.js. |
| 15 | + |
| 16 | +When writing TypeScript code with ESM, it can sometimes be desirable to import or export a type only in the type system. |
| 17 | +Code may wish to refer to a _type_, but not actually import or export a corresponding _value_. |
| 18 | + |
| 19 | +For that purpose, TypeScript 3.8 [added type-only imports and exports](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export) to the TypeScript language: |
| 20 | + |
| 21 | +```ts |
| 22 | +import type { SomeThing } from './some-module.js'; |
| 23 | +export type { SomeThing }; |
| 24 | +``` |
| 25 | + |
| 26 | +The key difference with `export type` and `import type` is that they _do not represent runtime code_. |
| 27 | +Attempting to use a _value_ imported as only a _type_ in runtime code will cause a TypeScript error: |
| 28 | + |
| 29 | +```ts twoslash |
| 30 | +import type { SomeThing } from './some-module.js'; |
| 31 | + |
| 32 | +new SomeThing(); |
| 33 | +// ~~~~~~~~~ |
| 34 | +// 'SomeThing' cannot be used as a value |
| 35 | +// because it was imported using 'import type'. |
| 36 | +``` |
| 37 | + |
| 38 | +TypeScript 4.5 also added [inline type qualifiers](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-5.html#type-modifiers-on-import-names), which allow for indicating that only some specifiers in a statement should be type-system-only: |
| 39 | + |
| 40 | +```ts |
| 41 | +import { type SomeType, SomeValue } from './some-module.js'; |
| 42 | +``` |
| 43 | + |
| 44 | +Type-only imports and exports are not emitted as runtime code when code is transpiled to JavaScript. |
| 45 | +This brings up two questions: |
| 46 | + |
| 47 | +- Why would you want to use these type-only imports and exports? |
| 48 | +- How can you enforce a project use them whenever necessary? |
| 49 | + |
| 50 | +Let's Dig In! |
| 51 | + |
| 52 | +<!--truncate--> |
| 53 | + |
| 54 | +## Benefits of Enforcing Type-only Imports/Exports |
| 55 | + |
| 56 | +### Avoiding Unintentional Side Effects |
| 57 | + |
| 58 | +Some modules in code may cause _side effects_: code that is run when the module is imported and causes changes outside the module. |
| 59 | +Common examples of side effects include sending network requests via `fetch` or creating DOM stylesheets. |
| 60 | + |
| 61 | +When projects include modules that cause side effects, the order of module imports matters. |
| 62 | +For example, some projects import the types of side-effect-causing modules in code that needs to run before those side effects. |
| 63 | + |
| 64 | +### Isolated Module Transpilation |
| 65 | + |
| 66 | +Import statements that only import types are generally removed when the TypeScript compiler transpiles TypeScript syntax to JavaScript syntax. |
| 67 | +The built-in TypeScript compiler is able to do so because it includes a type checker that knows which imports are of types and/or values. |
| 68 | + |
| 69 | +But, some projects use transpilers such as Babel, SWC, or Vite that don't have access to type information. |
| 70 | +These transpilers are sometimes referred to as _isolated module transpilers_ because they effectively transpile each module in isolation from other modules. |
| 71 | +Isolated module transpilers can't know whether an import is of a type, a value, or both. |
| 72 | + |
| 73 | +Take this file with exactly three lines of code: |
| 74 | + |
| 75 | +```ts |
| 76 | +// Is SomeThing a class? A type? A variable? |
| 77 | +// Just from this file, we don't know! 😫 |
| 78 | +import { SomeThing } from './may-include-side-effects.js'; |
| 79 | +``` |
| 80 | + |
| 81 | +If that `./may-include-side-effects.js` module includes side effects, keeping or removing the import can have very different runtime behaviors in the project. |
| 82 | +Indicating in code which values are type-only can be necessary for transpilers that don't have access to TypeScript's type system to know whether to keep or remove the import. |
| 83 | + |
| 84 | +```ts |
| 85 | +// Now we know this file's SomeThing is only used as a type. |
| 86 | +// We can remove this import in transpiled JavaScript syntax. |
| 87 | +import type { SomeThing } from './may-include-side-effects.js'; |
| 88 | +``` |
| 89 | + |
| 90 | +## Enforcing With typescript-eslint |
| 91 | + |
| 92 | +typescript-eslint provides two ESLint rules that can standardize using (or not using) type-only exports and imports: |
| 93 | + |
| 94 | +- [`@typescript-eslint/consistent-type-exports`](/rules/consistent-type-exports): Enforce consistent usage of type exports. |
| 95 | +- [`@typescript-eslint/consistent-type-imports`](/rules/consistent-type-imports): Enforce consistent usage of type imports. |
| 96 | + |
| 97 | +You can enable them in your [ESLint configuration](https://eslint.org/docs/latest/user-guide/configuring): |
| 98 | + |
| 99 | +```json |
| 100 | +{ |
| 101 | + "plugins": ["@typescript-eslint"], |
| 102 | + "rules": { |
| 103 | + "@typescript-eslint/consistent-type-exports": "error", |
| 104 | + "@typescript-eslint/consistent-type-imports": "error" |
| 105 | + } |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +With those rules enabled, running ESLint on the following code would produce a lint complaint: |
| 110 | + |
| 111 | +```ts |
| 112 | +import { GetString } from './types.js'; |
| 113 | +// All imports in the declaration are only used as types. Use `import type`. |
| 114 | + |
| 115 | +export function getAndLogValue(getter: GetString) { |
| 116 | + console.log('Value:', getter()); |
| 117 | +} |
| 118 | +``` |
| 119 | + |
| 120 | +The two rules can auto-fix code to use `type`s as necessary when ESLint is run on the command-line with `--fix` or configured in an editor extension such as the [VSCode ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint). |
| 121 | + |
| 122 | +For example, the `import` statement from earlier would be auto-fixed to: |
| 123 | + |
| 124 | +```ts |
| 125 | +import type { GetString } from './types.js'; |
| 126 | + |
| 127 | +export function getAndLogValue(getter: GetString) { |
| 128 | + console.log('Value:', getter()); |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +## More Lint Rules |
| 133 | + |
| 134 | +### `import` Plugin Rules |
| 135 | + |
| 136 | +[`eslint-plugin-import`](https://github.com/import-js/eslint-plugin-import) is a handy plugin with rules that validate proper imports. |
| 137 | +Although some of those rules are made redundant by TypeScript, many are still relevant for TypeScript code. |
| 138 | + |
| 139 | +Two of those rules in particular can be helpful for consistent `type` imports: |
| 140 | + |
| 141 | +- [`import/consistent-type-specifier-style`](https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/consistent-type-specifier-style.md): enforces consistent use of top-level vs inline `type` qualifier |
| 142 | +- [`import/no-duplicates`](https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-duplicates.md#inline-type-imports): warns against unnecessary duplicate imports (and with the `inline-type-imports` option can work in tandem with `import/consistent-type-specifier-style`). |
| 143 | + |
| 144 | +In conjunction with [`@typescript-eslint/consistent-type-imports`](/rules/consistent-type-imports), [`eslint-plugin-import`](https://github.com/import-js/eslint-plugin-import)'s rules can enforce your imports are always properly qualified and are written in a standard, predictable style (eg always top-level type qualifier or always inline type-qualifier). |
| 145 | + |
| 146 | +### Verbatim Module Syntax |
| 147 | + |
| 148 | +TypeScript 5.0 additionally adds a new [`--verbatimModuleSyntax`](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/#verbatimmodulesyntax) compiler option. |
| 149 | +`verbatimModuleSyntax` simplifies TypeScript's logic around whether to preserve imports. |
| 150 | +From the TypeScript release notes: |
| 151 | + |
| 152 | +> ...any imports or exports without a type modifier are left around. |
| 153 | +> Anything that uses the type modifier is dropped entirely. |
| 154 | +> |
| 155 | +> ```ts |
| 156 | +> // Erased away entirely. |
| 157 | +> import type { A } from 'a'; |
| 158 | +> |
| 159 | +> // Rewritten to 'import { b } from 'bcd';' |
| 160 | +> import { b, type c, type d } from 'bcd'; |
| 161 | +> |
| 162 | +> // Rewritten to 'import {} from 'xyz';' |
| 163 | +> import { type xyz } from 'xyz'; |
| 164 | +> ``` |
| 165 | +> |
| 166 | +> With this new option, what you see is what you get. |
| 167 | +
|
| 168 | +`verbatimModuleSyntax` is useful for simplifying transpilation logic around imports - though it does mean that transpiled code such as the may end up with unnecessary import statements. |
| 169 | +The `import { type xyz } from 'xyz';` line from the previous code snippet is an example of this. |
| 170 | +For the rare case of needing to import for side effects, leaving in those statements may be desirable - but for most cases you will not want to leave behind an unnecessary side effect import. |
| 171 | +
|
| 172 | +typescript-eslint now provides a [`@typescript-eslint/no-import-type-side-effects`](/rules/no-import-type-side-effects) rule to flag those cases. |
| 173 | +If it detects an import that only imports specifiers with inline `type` qualifiers, it will suggest rewriting the import to use a top-level `type` qualifier: |
| 174 | +
|
| 175 | +```diff |
| 176 | +- import { type A } from 'xyz'; |
| 177 | ++ import type { A } from 'xyz'; |
| 178 | +``` |
| 179 | +
|
| 180 | +## Further Reading |
| 181 | + |
| 182 | +You can read more about the rules' configuration options in their docs pages. |
| 183 | +See [our Getting Started docs](/getting-started) for more information on linting your TypeScript code with typescript-eslint. |
| 184 | + |
| 185 | +## Supporting typescript-eslint |
| 186 | + |
| 187 | +If you enjoyed this blog post and/or or use typescript-eslint, please consider [supporting us on Open Collective](https://opencollective.com/typescript-eslint). |
| 188 | +We're a small volunteer team and could use your support to make the ESLint experience on TypeScript great. |
| 189 | +Thanks! 💖 |
0 commit comments