Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

request: add { importsNotUsedAsValues: preserve-values } to compilerOptions #43687

Closed
5 tasks done
milahu opened this issue Apr 15, 2021 · 7 comments
Closed
5 tasks done
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@milahu
Copy link

milahu commented Apr 15, 2021

Suggestion

im looking for a { importsNotUsedAsValues: 'preserve-values' } compiler-option, where

1. all type-imports are removed, including "empty imports" like import "./some" and import "./where.d.ts"
2. unused imports are preserved, including maybe-type-imports

this would make it much easier to compile code-fragments in isolation
where ts has no access to other files, and no access to the fragment's context

1. type-imports should be fully removed

import { SomeType } from "./types"
var someVar: SomeType; // here ts can see that SomeType is a type

// ... is currently transpiled to ...

import "./types";
var someVar;

2. unused imports should be preserved

import { NotUsed } from "./mixed"
// NotUsed is not used anywhere, so its value or type

// ... is currently transpiled to ...

import "./mixed";

to support the infamous export { SomeThing } from './some-where' case
where ts.transpileModule cannot tell whether SomeThing is a type or value:

use a fault-tolerant module loader, like

// note: top level await only works in modules
var importedModule = await (async () => {
  try { return await import('./some-module.js'); }
  catch (error) { console.warn(error.message); }
  return {};
})();

this is needed for development mode, where tree-shaking is disabled
so imports from type files would give fatal errors

deprecated: transpile type exports to "dummy exports"

edit: this is deprecated, since it breaks with external modules

external modules are not compiled in the current build pipeline
so there is no way we can generate "dummy exports" for all maybe-type-imports

in case SomeThing is a type
ts.transpileModule would have to generate "dummy exports"
like export const SomeThing = null; in some-where.js

... to avoid the runtime error SomeThing is not exported by some-where
or no such file: some-where

export type SomeType = number;

// ... would be transpiled to ...

export const SomeType = null;

adding such dummy exports should be safe
since collisions between value IDs and type IDs are not possible in ts
and IDs are (usually) scoped to modules

this solution is "clean", cos the added dead-code is removed later in the build pipeline (tree-shaking in production mode)

compiler call (playground on codesandbox)

var result = ts.transpileModule(source, {
  fileName: "test.ts",
  compilerOptions: {
    target: "es2015",
    importsNotUsedAsValues: "preserve-values", // new
  }
});

🔍 Search Terms

single file compilation
ts.transpileModule
remove only type imports
keep unused value imports
generate dummy exports for types

✅ Viability Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
    • yes: the "dummy exports" would be removed later in the build pipeline (dead code elimination, tree shaking)
      • yes: goal 4. Emit clean, idiomatic, recognizable JavaScript code.
      • yes: goal 7. Preserve runtime behavior of all JavaScript code.
      • yes: goal 9. Use a consistent, fully erasable, structural type system.
    • yes: non-goal 4. Provide an end-to-end build pipeline. Instead, make the system extensible so that external tools can use the compiler for more complex build workflows.

⭐ Suggestion

📃 Motivating Example

💻 Use Cases

references

single file transpile: sveltejs/svelte-preprocess#318 (comment)
dummy exports: https://stackoverflow.com/a/52260432/10440128
api docs: https://www.typescriptlang.org/tsconfig#importsNotUsedAsValues

@andrewbranch
Copy link
Member

I would be open to a mode where

  1. import type declarations are removed (as they are now)
  2. import (without type) declarations are not touched whatsoever, regardless of usage—that is, imported names are not removed.
  3. It is an error to import anything that is only a type without using import type. (Contrast to --importsNotUsedAsValues=error, which allows pure types to be imported without import type as long as the same import declaration also imports at least one value.)

Would that solve the svelte problem?

@andrewbranch
Copy link
Member

andrewbranch commented Apr 15, 2021

Note: my counter-suggestion is essentially #43393

@andrewbranch andrewbranch added In Discussion Not yet reached consensus Suggestion An idea for TypeScript labels Apr 15, 2021
@milahu
Copy link
Author

milahu commented Apr 15, 2021

2. import (without type) declarations are not touched whatsoever, regardless of usage—that is, imported names are not removed.
3. It is an error to import anything that is only a type without using import type

nah, we want to mix types and values like import { SomeType, SomeValue } from './some-where'

ts.transpileModule has no access to other files, so we must handle the special case
where we dont know whether its a type-import or value-import
and where type-imports can fail when the *.js file does not exist

@andrewbranch
Copy link
Member

But you don’t need to mix types and values in your imports, right? In this mode of checking, transpilers should never need to guard against some export or file not existing because it was just a type.

@milahu
Copy link
Author

milahu commented Apr 15, 2021

But you don’t need to mix types and values in your imports, right?

no, but we want to ... and we want to avoid the workaround that

uses the full markup, strips the script and style tags and pass it to svelte.compile to get a report of all used variables in the template. This list of variables is appended to the TypeScript code to allow the TypeScript compiler to correctly handle imports.

so that to typescript, imports appear to be used, so they are not removed in import eliding

that workaround is expensive cos it involves double-parsing the html markup
not sure if that double-parsing can be avoided ...

but generally, this would be a "nice to have" feature for the ts compiler
to make it easier to compile isolated code fragments

@andrewbranch
Copy link
Member

I see. Well, I think the answer to this suggestion is probably no. What I’m proposing fixing is

a transformer was written to re-add imports which to TypeScript look unused but are actually used in the template. This means that people need to be very careful about how to write their imports, which can be especially cumbersome if a type import is from the same file as a real import.

If we implemented my proposal, it would have two consequences:

  1. That transformer could be removed.
  2. You would no longer need to be careful about combining type and value imports, because it would be enforced that you don’t do it.

That seems much better than the status quo, because it would improve transpilation performance and guard against a pitfall that seems very easy to fall into.

@andrewbranch
Copy link
Member

I’m going to reopen #43393 and move the conversation to there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

2 participants