#Why TypeScript `tsc` remove unused imports by default?

75 messages · Page 1 of 1 (latest)

gleaming breach
#

I'm reading the docs of verbatimModuleSyntax, which links to now deprecated importsNotUsedAsValues. I found its statement for the value remove a bit confusing since I didn't know whether TypeScript compiler remove unused import by default:

remove: The default behavior of dropping import statements which only reference types.

I thought that TypeScript would not remove unused imports by reading this statement itself, but this is not true. After testing on the playground, I found that TypeScript remove unused import by default.

I found nowhere in the documentation saying about this default behaviour of removing unused imports. There are many cases we import something just for the side-effect, and removing unused imports by default is inappropriate for me due to this reason. My question is simple: Where is the statement in TypeScript docs website mentioning about this default behaviour?

urban musk
#

if TS removes your import because it's not used and breaks your code, then you're probably doing it wrong

#

e.g. import {} from "./file"; will be removed, since the named import doesn't actually import anywhere

#

but if you initialize variables in ./file.ts, then you should use an import "./file"; instead, which won't be removed

#

@gleaming breach

gleaming breach
#

[...] then you should use an import "./file" instead, [...]

Hi, Ascor! Thank for the tip. I read about something similar when I dived into some PRs on both TypeScript- & Babel-side hours ago. I self-ask-and-answered my original confusion hours ago, if you want to make correction or maybe provide more advice there: https://stackoverflow.com/questions/78135027/will-typescript-compiler-still-remove-unused-imports-by-default-after-deprecatin.

#

I will mark this thread as resolved once I finish my reading of related pages and clean up my notes.

urban musk
#

made it much clearer by seeing yourt code example

#
import path from 'path';

const anExampleVariable = "Hello World"
console.log(anExampleVariable)

is indeed less than ideal

#

if do do need to load a module/file but without actually importing anything, you should use the import "path"; syntax

#

thaty's what it's for

#

By default, TypeScript does something called import elision. Basically, if you write something like
[...]
But it does add a layer of complexity for certain edge cases. For example, notice there’s no statement like import "./car"; - the import was dropped entirely. That actually makes a difference for modules that have side-effects or not.
[...]
TypeScript 5.0 introduces a new option called --verbatimModuleSyntax to simplify the situation. The rules are much simpler - any imports or exports without a type modifier are left around. Anything that uses the type modifier is dropped entirely.

#

notice how the behavior of --verbatimModuleSyntax changes depending on what/how you import stuff

gleaming breach
#

Understood. The new option is meant to replace many other ones. On the other hand, I found their description hard to read: https://www.typescriptlang.org/tsconfig#importsNotUsedAsValues.

error: This preserves all imports (the same as the preserve option), but will error when a value import is only used as a type. This might be useful if you want to ensure no values are being accidentally imported, but still make side-effect imports explicit.

At first it seems that they can just say "when importing a type" instead of the part I bolded. Anyway, still trying to read the related options. I understood that verbatimModuleSyntax in plain English is that I have to be aware of type import and "how they're declared", e.g. declared as class. Again, I found they making it hard to read.

urban musk
#

¯_(ツ)_/¯

#

maybe you just aren't used to the technical TS vocabulary

#

like, I have no problem understanding what it means, and have a clear idea of what it does

#

without every having used it once

#

but yeah

#

tldr.
importsNotUsedAsValues keeps all import statement,
even if it points to a .d.ts file that won't exist in JS,
so there is a chance it causes errors

gleaming breach
#

Is it correct to say that in TypeScript's jargon, everything imported is called value? And then they distinguish values into "values used as a type" and simply "values"? Anyway, I have to make an explanation for myself.

urban musk
#

Is it correct to say that in TypeScript's jargon, everything imported is called value?
no, not necessaraily
values are object that exist both at compile time and at runtime (variabale, function, class, enum, etc.)
but there are also "things" that only exist at compile time, like types, interfaces, etc.

#

there are different use cases for imports:

  • import a value, and use it
  • import a type, and use it
  • import a value, but not use it for it's value, but for it's type (used in conjunction with the typeof operator)
#
import { myFunc } from "./foo";
myFunc()
import { type MyType } from "./foo";
const foo: MyType = { foo: true };
import { myFunc } from "./foo";
const func: typeof myFunc = () => {

};
#

when compiling, in some cases, you can get rid of some imports

#

as in

import { type MyType } from "./foo";
const foo: MyType = { foo: true };

since MyType is a type/interface
and those don't exist at runtime
so they can safely be "removed"
so you end up with
import { } from "./foo";

now the real question is: is it safe to remove import { } from "./foo";? does the import have side effects?

#

that's what all those options are for

gleaming breach
#

Thank you for the detailed explain. My apologies for late reply because I was trying to at least having my own thoughts organized before reading new info.

#

(Kindly tagging @urban musk )
Now I have a question regarding your words and examples:

import a value, but not use it for it's value, but for it's type

Is it correct to say that your third example with typeof is the only usage that fits your definition here?

Use my contrived example:

  1. If I toggle the option preserveValueImports, while I use OnlyUsedAsType as type, the output code changed.
  2. If I add type keyword into the import statement/expression: import { type OnlyUsedAsType, ..., and try 1. again, then I got identical output.

I'm thinking about whether I can treat it like const in C++: mark it as const doesn't mean that the underlying value is really a const. It's just that from now on it's treated as a constant. So here I could say type OnlyUsedAsType means that I'm treating it as a type so it will always be removed. On the 1. case, when preserveValueImports is true, the import of OnlyUsedAsType is kept even though it's used as a type.

thick sequoiaBOT
#

@gleaming breach Here's a shortened URL of your playground link! You can remove the full link from your message.

itsrainnnnyng#0

Preview:```ts
import {OnlyUsedAsType, AlsoUsedAsValue} from ""
import {UsedAsValue} from ""

declare const a: OnlyUsedAsType
const b = UsedAsValue
const c = AlsoUsedAsValue```

urban musk
#

@gleaming breach I'm not sure what you are asking but here are my toughts on this:

  1. preserveValueImports and importsNotUsedAsValues are deprecated
    you shouldn't be using them anymore
    you should forget about them

  2. if you need to initialize something in a file, and you do that by importing that file (your import has side effects), use the

import "./file";
``` syntax

and not the
```ts
import foo from "./file";
``` syntax, even more if you aren't using `foo`
gleaming breach
#

quick reply for 1.: sure. But while it's removed it might be worth (at least for me) to think about it regard I might encounter some old versions of typescript. But I accept the advice.

urban musk
#

I might encounter some old versions of typescript
nah, usually there is no reason to use or stay on an older version of TypeScript

#

it's very different from other programming languages where you stick to one version of the language

#

in TS, it's recommended to always upgrade when you can

#

(unless you have a giant codebase, in which case it requires a lot of effort)

#

but yeah:

importsNotUsedAsValues

  • remove
import { Foo } from "./foo";
const foo: Foo = { foo: "foo" };
``` becomes
```js
const foo = { foo: "foo" };
  • preserve
import { Foo } from "./foo";
const foo: Foo = { foo: "foo" };
``` becomes
```js
import { } from "./foo";
const foo = { foo: "foo" };
  • error
import { Foo } from "./foo";
const foo: Foo = { foo: "foo" };
``` becomes
```js
import { } from "./foo"; // <-- error
const foo = { foo: "foo" };
#

guess it does the same with default imports/exports

#

default imports/exports was probably the intended target for that flag tbh

#

preserveValueImports

false

import { Animal } from "./animal.js";
eval("console.log(new Animal().isDangerous())");
``` becomes
```js
eval("console.log(new Animal().isDangerous())");

true

import { Animal } from "./animal.js";
eval("console.log(new Animal().isDangerous())");
``` becomes
```js
import { Animal } from "./animal.js";
eval("console.log(new Animal().isDangerous())");
gleaming breach
#

In fact, what I stuck on is actually the behaviour of preserveValueImports. With my example above while it is used as a type it's still not removed when this flag is set to true.

#

Oops, was typing, let me read your example and many thanks 🙂

#
import { Animal } from "./animal.js";
declare const cat: Animal;

How about this one? with true. And I'm still confused by its name "ValueImports". (not saying it's wrong. But I think I haven't understood it correctly)

urban musk
gleaming breach
#

It's kept when I testing it in the playground. Let me check again.

thick sequoiaBOT
#
itsrainnnnyng#0

Preview:ts import {Animal} from "./animal.js" declare const cat: Animal

urban musk
#

hum monkaHmm interesting

#

you might run into problems with that option tho

#

that's why it got deprecated I think

#

doesn't do what ppl expect

#

one more reason not to use it

gleaming breach
#

I understood that this option is now deprecated and yes I'm a nightly user in mind. But the problem (my problem) is that this option is deprecated not because it's wrong, but because they want to simplify things with a composite option verbatim... (forgot it's name)

urban musk
#

yes, the verbatim option is much better

#

and acts in a more straightforward way

#

but tbh, I recommend sticking with the default options up to the points they aren't enough

#

then try what option could solve your problem

gleaming breach
urban musk
#

I mean, I understand what it does now

#

but you just need to be very cautious when you use it

#

and mark all your type import as types

#

otherwise you'll run into some troubles

#

also

#

idk if it's because of the playground, but maybe in a real project, tsc can check if the import is a type or a value

#

and remove it if it's only a type

#

the playground isn't ideal to test all the module config options

gleaming breach
gleaming breach
#

My argument above with playground is wrong, after testing.

urban musk
#

yeah, types are always erased
doesn't make sense to keep them, they don't exist at runtime

gleaming breach
#

Cannot even trust the playground.

urban musk
#

that's just because the playground can only act as a single file/module

#

and if you try to import stuff that does not exist, the typescript cannot check what type that thing is

#

and cannot guarantee corectness for the imports

gleaming breach
#

I found that they also provide something called bug-workbench, which supports multi-file testing but no syntax-highlighting.