-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
importsNotUsedAsValues: preserve is not preserving #43393
Comments
Hmm, it was an intentional decision that |
Ok, I think that's fair. I was hoping this feature would achieve preservation of the original JS code, i.e. avoiding dead identifier elimination. But I see it does not have that ambition so this is not a bug and I will close it. And having written all this up, I'm now thinking that was an unreasonable assumption on my part 😉 As background, this issue was detected because we ban the Answering your question, I think stringified execution is the only practical case. In this case, it seems there is no way to get TypeScript to emit the legitimate input JS code even if you disable the checker. So instead, when you detect in your tests or runtime execution that the code is faulty, the workaround is to add a redundant usage site to prevent the compiler from eliminating it. IMO this is a super-mild case of breaking the JS+Types model because we can't passthrough plain JS. It's an unavoidable transform. Maybe it's also a category mismatch because TypeScript is kinda doing the work of a minifier. Other simplistic forms of dead code, such as unused |
So this same thing came up in the context of Svelte’s transpilation process—they had to write a custom transformer to re-add back imports that were removed because they look unused, but were in fact used in template code, which TS doesn’t understand. Moving some conversation from #43687 and continuing it here. In #43687 I proposed a mode where:
because
|
Thank you for reviving the issue @andrewbranch. I see how Svelte needs this same "just remove the types" mode. I like where this is going. The mode you proposed above sounds reasonable and will solve the problem. It means the whole import statement is either retained or eliminated based on whether it's a type-only import or not. There is a potential extension to the above mode. Which is to introduce new syntax for explicit type-only identifiers. I read this is already supported by Flow and Hegel but cannot find supporting documentation. import { a, type b, c, type d } from "module"; I think we would still need your new mode in order to error in the case where Please say if you think this extension is better handled in a separate issue - I would be happy to create it. |
Thank you for reconsidering this @andrewbranch ! What you wrote is exactly what would be needed. An illustrative example: <script lang="ts">
import { value, valueOnlyUsedInTemplate } from './somewhere';
import type { aType } from './somewhere';
const foo: aType = value;
</script>
{foo} {valueOnlyUsedInTemplate} The preprocessor invokes import { value, valueOnlyUsedInTemplate } from './somewhere';
import type { aType } from './somewhere';
const foo: aType = value; Given the current mechanics of TS transpilation, both
I think tackling this is closely related to the intention in #39432 which I left a comment on just now. |
edit: seems like i misunderstoood
as
supporting mixed imports would be a bit more complex
let me repeat
-
even worse:
my suggestion was to add a (rather opinionated) logic for the var result = ts.transpileModule(source, {
fileName: "test.ts",
compilerOptions: {
target: "es2015",
importsNotUsedAsValues: function({ importDeclaration, compilerOptions }) {
// ...
},
}
}); where importDeclaration examplesconst testCases = [
{
_ts: `import defaultRename from './some-where';`,
importDeclaration : {
name: { escapedText: "defaultRename" },
moduleSpecifier: { text: "./some-where" },
namedBindings: undefined,
}
},
{
_ts: `import defaultRename, { key1, key2 } from './some-where';`,
importDeclaration : {
name: { escapedText: "defaultRename" },
namedBindings: {
elements: [
{ name: { escapedText: "key1" } },
{ name: { escapedText: "key2" } },
]
},
moduleSpecifier: { text: "./some-where" },
}
},
{
_ts: `import defaultRename, { key1 as key1rename, key2 as key2rename } from './some-where';`,
importDeclaration : {
name: { escapedText: "defaultRename" },
namedBindings: {
elements: [
{ propertyName: { escapedText: "key1" }, name: { escapedText: "key1rename" } },
{ propertyName: { escapedText: "key2" }, name: { escapedText: "key2rename" } },
]
},
moduleSpecifier: { text: "./some-where" },
}
},
{
_ts: `import * as moduleRename, { key1 as key1rename, key2 as key2rename } from './some-where';`,
importDeclaration : {
namedBindings: {
name: { escapedText: "moduleRename" },
elements: [
{ propertyName: { escapedText: "key1" }, name: { escapedText: "key1rename" } },
{ propertyName: { escapedText: "key2" }, name: { escapedText: "key2rename" } },
]
},
moduleSpecifier: { text: "./some-where" },
}
},
{
_ts: `import { someThing } from './some-where';`,
importDeclaration : {
moduleSpecifier: { text: "./some-where" },
namedBindings: {
elements: [
{ name: { escapedText: "someThing" } },
]
},
}
},
{
_ts: `import { someType } from './some-where';`,
importDeclaration : {
namedBindings: {
elements: [
{ name: { escapedText: "someType", usedAsType: true } },
]
},
moduleSpecifier: { text: "./some-where" },
}
},
{
_ts: `import { someValue } from './some-where';`,
importDeclaration : {
namedBindings: {
elements: [
{ name: { escapedText: "someType", usedAsValue: true } },
]
},
moduleSpecifier: { text: "./some-where" },
}
},
]; the callback function could look like 50 lines monster (live demo) callback function examplevar importsNotUsedAsValuesCallback = function({ importDeclaration, compilerOptions }) {
const { name, namedBindings, moduleSpecifier } = importDeclaration;
if (moduleSpecifier.text.endsWith('.d.ts')) return false; // elide full import
const valueImports = []; const dynamicImports = [];
if (namedBindings?.elements) {
// import { elem1 } from './some-where'; // -> name: { escapedText: "key1" }
// import { elem1 as newname1 } from './some-where'; // -> name: { escapedText: "newname1" }, propertyName: { escapedText: "key1" }
for (const element of namedBindings.elements) {
const { name, propertyName } = element;
//console.log('element = ' + JSON.stringify(element));
if (name.usedAsType == true) {
//console.log(`elide type import: ${name.escapedText}`);
continue; // elide import
}
const array = (name.usedAsValue == true)
? valueImports // keep import
: dynamicImports; // type or value, we dont know
array.push({ dst: name.escapedText, src: propertyName?.escapedText });
}
}
if (name) { // import defaultRename from './some-where'; -> name: { escapedText: "defaultRename" }
console.log('name = ' + JSON.stringify(name));
if (name.usedAsType == true) { console.log(`elide full type import: ${name.escapedText}`); return false; } // elide full import
if (name.usedAsValue == true) return true; // keep full import
// TODO dynamic import
dynamicImports.push({ dst: name.escapedText, src: 'default' });
}
console.log(`found ${valueImports.length} value imports + ${dynamicImports.length} dynamic imports`);
if (valueImports.length == 0 && dynamicImports.length == 0) return false; // elide full import
if (valueImports.length > 0) {
// the import path exists, no need for dynamic import
return valueImports; // TS will convert the array to JS import code
}
// dynamic import
const importFullModule = Boolean(namedBindings?.name);
const importProps = valueImports.concat(dynamicImports);
const importPropsJs = '{ ' + importProps.map(({ dst, src }) => (src ? `${src}: ${dst}` : dst)).join(', ') + ' }';
const importDst = importFullModule ? namedBindings.name.escapedText : importPropsJs;
return `\
// note: top level await only works in modules
const ${importDst} = await (async () => {
try { return await import(${JSON.stringify(moduleSpecifier.text)}); }
catch (error) { console.warn(error.message); }
return {};
})();
${
(importFullModule && importProps.length > 0)
? `const ${importPropsJs} = ${importDst};`
: ''
}
`;
}; for unused imports, where TS cannot tell if type or value, import * as moduleRename, { key1 as key1rename, key2 as key2rename } from './some-where';
// ... is transpiled to ...
const moduleRename = await (async () => {
try { return await import("./some-where"); }
catch (error) { console.warn(error.message); }
return {};
})();
const { key1: key1rename, key2: key2rename } = moduleRename; note: this workaround is only needed for development mode. |
Thinking out loud about @robpalme’s suggestion, which ties into the DX concerns in sveltejs/svelte-preprocess#318 (comment): I do think adding a import type foo, { Bar } from './mod' Does |
These are valid concerns, I indeed would also be confused about the ambiguity. If you really want to add support, it could maybe tweaked like this: import { Bar, type baz }, type foo from './mode'; // Bar value import, foo/baz type import So a default import is a type import inside a mixed import if it's last in the chain. |
That’s a step too far away from standard ES syntax. We were comfortable with a |
This all makes sense. Do you think we must find a way to support the concise type-only default import in combination with value imports? It feels kinda unnecessary to me given we can already do (non-concise) rebinding which makes it all unambiguous. import { type default as foo, Baz } from './mod' |
I’ve been thinking about this new mode, which will probably be a new value for import d from './child';
eval("d"); Suppose you compiled this with "use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const child_1 = __importDefault(require("./child"));
eval("d"); because any actual trackable usage of On the other hand, ever since type-only imports were added, we’ve had a consistent stream of people who are frustrated that they can’t opt into a mode where all types must use a type-only import and never be combined with value imports, which is what we’re now thinking of offering—but as far as I could tell, most of the people who have previously asked for that have been motivated by, well, what I consider to be linty pedantry, not because of any sort of runtime constraint. If we were to require es2015+ for this flag, it would probably rain on the parades of some people who want this separation for purely stylistic/DX reasons.
|
cleaner than my hacky fault-tolerant-dynamic-imports, i have to admit ; ) mixed imports should be auto-fixable by eslint or IDEs, since they can detect type-imports vs value-imports |
I'm also in favor of es2015+ . As you said, this is not to please stylistic preferences but to solve transpilation constraints. And through time this problem will hopefully solve itself when the ecosystem as a whole has moved towards ESM. |
Requiring The primary use-case here is to minimize source transforms and just erase the type syntax, to cater for code/tools whose runtime behavior is broken by todays dead-identifier transform. I foresee it being used most commonly in toolchains that also use There's a quote from Paul Rudd in Forgetting Sarah Marshall that I think applies here: "the less you do, the more you do" The stylistic/lint use-case is new to me. Personally I feel similar to @dummdidumm and would prioritize functional needs over aesthetics. Implementing this for CommonJs, whilst feasible, feels like unnecessary work to me (and maybe goes against the Paul Rudd principle of "do less"?). If the demand for this feature with non-ESM module targets arises in future, maybe the requesters could create an ESLint rule as @milahu suggests. |
One question I had, which may be difficult to answer, is whether people who check with TypeScript but transpile with Babel or something else actually set |
I can't give a general answer, but in my specific case, in which there is no down-levelling because we use a modern engine, we always use This is used in both the build tools' main API usage and also in the tsconfig we write to disk to drive VSCode which contains |
Bug Report
My interpretation of the
"importsNotUsedAsValues": preserve
option is that the goal is to emit value imports as-is. So that you can rely on the JS emit reflecting the JS you write. Here is a case that seems to violate that.🔎 Search Terms
importsNotUsedAsValues preserve import binding missing skipped omitted import for side-effects JS+types
🕗 Version & Regression Information
⏯ Playground Link
Playground link with relevant code
💻 Code
🙁 Actual behavior
JS emit for
main.ts
is missing the import binding.🙂 Expected behavior
The value import should be preserved.
The text was updated successfully, but these errors were encountered: