What are the real use cases of type-only imports that justify the added verbosity?

7.7k views Asked by At

When I upgraded to the latest version of TypeScript and found out about type-only imports, I thought it was super cool and started using it everywhere.

After a while setting up type-only imports, I soon realised I was getting quite more verbosity and "dirty code" I had expected.

Without type-only imports:

import { SomeType, someFunction, SomeClassToInstantiate } from '@my-app/lib1';
import { OtherType, otherFunction, OtherClassToInstantiate } from '@my-app/lib2';

With type-only imports:

import type { SomeType } from '@my-app/lib1';
import { someFunction, SomeClassToInstantiate } from '@my-app/lib1';
import type { OtherType } from '@my-app/lib2';
import { otherFunction, OtherClassToInstantiate } from '@my-app/lib2';

Basically, a lot of my imports get duplicated, and it's difficult to track whether I am importing everything in the correct way (the compiler flags if I am importing as type something that I am physically using in the file - but the opposite doesn't hold, I didn't find any tool to flag that an import is only used as type and should be switched to type-only import).

Maybe I am noticing this problem more because I am using NX and a lot of my app's code comes from the libs' barrel files; i.e. it often happens that both a type and a non-type have to be imported from the same module.

So I was wondering, what actual advantages do I get from using type-only imports everywhere like that? Are there instead specific circumstances in which it's definitely helpful to use it, and it can be bypassed in all the other cases?

If the answer to my previous question is that it should always used when possible, do you know any linting rule to enforce type imports when they can be used?

I don't like the confusion of having nearly double the imports than before, without even knowing that they are consistently split across regular / type-only imports everywhere.

2

There are 2 answers

2
Oleg Valter is with Ukraine On

Are there instead specific circumstances in which it's definitely helpful to use it, and it can be bypassed in all the other cases?

Although it does not hurt to use type-only imports unconditionally (for consistency's sake), there are, indeed, specific circumstances where they are required. The following table compares different option combinations in terms of use cases:

isolatedModules/preserveValueImports true false
true Type-only imports are required as single-file processing lacks full type system information to elide type imports.
Example
Type-only exports are required if exporting types.
Example
false Preferencial, type-only imports will be elided Preferencial, type-only imports will be elided

The most common use case for them is when both isolatedModules and preserveValueImports are enabled, which is explicitly documented:

When combined with isolatedModules: imported types must be marked as type-only because compilers that process single files at a time have no way of knowing whether imports are values that appear unused, or a type that must be removed in order to avoid a runtime crash.

Another use case is when isolatedModules module is enabled (regardless of the preserveValueImports option), and you are exporting types (imported or not), which is also documented:

Single-file transpilers don’t know whether someType produces a value or not, so it’s an error to export a name that only refers to a type.

If your config has preserveValueImports enabled, type-only imports are also useful to force the elision of imports that are only used in type positions that would not be a runtime error if preserved (in the following example, import type will allow the compiler to elide the import entirely):

import { ESLint } from "eslint"; // ESLint is a class
let esl: ESLint;
export { esl };

As for linting, @typescript-eslint/eslint-plugin for ESLint has 2 rules that govern the usage of type-only imports and exports:


On an off-note, since 4.5, TypeScript has type modifiers on import names that were added specifically to address the verbosity of type-only imports, so you no longer need to split imports:

import { someFunction, SomeClassToInstantiate, type SomeType } from '@my-app/lib1';
0
NeoZoom.lua On

I just finished my fun reading all related issues/proposals/PRs. This is a kindly summary for lazy readers:

  1. The type-only imports/exports is a feature proposed to solve a specific problem/trouble. It's not just for fun or providing convenient: TypeScript used to have trouble to satisfy both camps:

camp1: please DON'T help me remove you-guess-unused imports, because I want side-effects:

// input
import { OnlyUsedAsType } from "sideEffectMod";
import { UsedAsValue } from "basicMod";

declare const a: OnlyUsedAsType;
const b = UsedAsValue;

// output                       <---- oh no, `sideEffectMod` is removed!
import { UsedAsValue } from "";
const b = UsedAsValue;

camp2: please DO help me remove you-guess-used imports, because I'm non-typecheck transpiler on isolatedModules:

import { SomeType } from "someModule"; // <---- oh no, I have to assume it is not a type
export { SomeType };                   // <---- too.
let x: SomeType | undefined;

export { SomeOtherType } from "someModule"; // <---- too.
  1. So, they decided to create 1) a new syntax -- type-only imports/exports, i.e. import type {...} and 2) an option to preserve side-effects, i.e. importsNotUsedAsValues.

So, happy ending, as both camps were satisfied, right...? Nope. But for the sake of beginner-friendly, let's end it here :)