Skip to content

Commit 2170e6c

Browse files
authored
Proposal: Always allow type-only imports to reference .ts extensions (#54746)
1 parent 55fcee4 commit 2170e6c

9 files changed

+271
-2
lines changed

src/compiler/checker.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4908,8 +4908,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
49084908
}
49094909
}
49104910
else if (resolvedModule.resolvedUsingTsExtension && !shouldAllowImportingTsExtension(compilerOptions, currentSourceFile.fileName)) {
4911-
const tsExtension = Debug.checkDefined(tryExtractTSExtension(moduleReference));
4912-
error(errorNode, Diagnostics.An_import_path_can_only_end_with_a_0_extension_when_allowImportingTsExtensions_is_enabled, tsExtension);
4911+
const importOrExport =
4912+
findAncestor(location, isImportDeclaration)?.importClause ||
4913+
findAncestor(location, or(isImportEqualsDeclaration, isExportDeclaration));
4914+
if (!(importOrExport?.isTypeOnly || findAncestor(location, isImportTypeNode))) {
4915+
const tsExtension = Debug.checkDefined(tryExtractTSExtension(moduleReference));
4916+
error(errorNode, Diagnostics.An_import_path_can_only_end_with_a_0_extension_when_allowImportingTsExtensions_is_enabled, tsExtension);
4917+
}
49134918
}
49144919

49154920
if (sourceFile.symbol) {

src/services/codefixes/importFixes.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
arrayFrom,
66
CancellationToken,
77
cast,
8+
changeAnyExtension,
89
CodeAction,
910
CodeFixAction,
1011
CodeFixContextBase,
@@ -42,10 +43,13 @@ import {
4243
getExportInfoMap,
4344
getMeaningFromDeclaration,
4445
getMeaningFromLocation,
46+
getModeForUsageLocation,
4547
getNameForExportedSymbol,
4648
getNodeId,
49+
getOutputExtension,
4750
getQuoteFromPreference,
4851
getQuotePreference,
52+
getResolvedModule,
4953
getSourceFileOfNode,
5054
getSymbolId,
5155
getTokenAtPosition,
@@ -1356,6 +1360,15 @@ function promoteFromTypeOnly(changes: textChanges.ChangeTracker, aliasDeclaratio
13561360

13571361
function promoteImportClause(importClause: ImportClause) {
13581362
changes.delete(sourceFile, getTypeKeywordOfTypeOnlyImport(importClause, sourceFile));
1363+
// Change .ts extension to .js if necessary
1364+
if (!compilerOptions.allowImportingTsExtensions) {
1365+
const moduleSpecifier = tryGetModuleSpecifierFromDeclaration(importClause.parent);
1366+
const resolvedModule = moduleSpecifier && getResolvedModule(sourceFile, moduleSpecifier.text, getModeForUsageLocation(sourceFile, moduleSpecifier));
1367+
if (resolvedModule?.resolvedUsingTsExtension) {
1368+
const changedExtension = changeAnyExtension(moduleSpecifier!.text, getOutputExtension(moduleSpecifier!.text, compilerOptions));
1369+
changes.replaceNode(sourceFile, moduleSpecifier!, factory.createStringLiteral(changedExtension));
1370+
}
1371+
}
13591372
if (convertExistingToTypeOnly) {
13601373
const namedImports = tryCast(importClause.namedBindings, isNamedImports);
13611374
if (namedImports && namedImports.elements.length > 1) {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
b.ts(2,16): error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.
2+
b.ts(3,30): error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.
3+
b.ts(5,25): error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.
4+
c.ts(2,16): error TS2846: A declaration file cannot be imported without 'import type'. Did you mean to import an implementation file './a.js' instead?
5+
c.ts(3,30): error TS2846: A declaration file cannot be imported without 'import type'. Did you mean to import an implementation file './a.js' instead?
6+
c.ts(5,25): error TS2846: A declaration file cannot be imported without 'import type'. Did you mean to import an implementation file './a.js' instead?
7+
8+
9+
==== a.ts (0 errors) ====
10+
export class A {}
11+
12+
==== a.d.ts (0 errors) ====
13+
export class A {}
14+
15+
==== b.ts (3 errors) ====
16+
import type { A } from "./a.ts"; // ok
17+
import {} from "./a.ts"; // error
18+
~~~~~~~~
19+
!!! error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.
20+
import { type A as _A } from "./a.ts"; // error
21+
~~~~~~~~
22+
!!! error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.
23+
type __A = import("./a.ts").A; // ok
24+
const aPromise = import("./a.ts"); // error
25+
~~~~~~~~
26+
!!! error TS5097: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.
27+
28+
==== c.ts (3 errors) ====
29+
import type { A } from "./a.d.ts"; // ok
30+
import {} from "./a.d.ts"; // error
31+
~~~~~~~~~~
32+
!!! error TS2846: A declaration file cannot be imported without 'import type'. Did you mean to import an implementation file './a.js' instead?
33+
import { type A as _A } from "./a.d.ts"; // error
34+
~~~~~~~~~~
35+
!!! error TS2846: A declaration file cannot be imported without 'import type'. Did you mean to import an implementation file './a.js' instead?
36+
type __A = import("./a.d.ts").A; // ok
37+
const aPromise = import("./a.d.ts"); // error
38+
~~~~~~~~~~
39+
!!! error TS2846: A declaration file cannot be imported without 'import type'. Did you mean to import an implementation file './a.js' instead?
40+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//// [tests/cases/conformance/externalModules/typeOnly/allowsImportingTsExtension.ts] ////
2+
3+
//// [a.ts]
4+
export class A {}
5+
6+
//// [a.d.ts]
7+
export class A {}
8+
9+
//// [b.ts]
10+
import type { A } from "./a.ts"; // ok
11+
import {} from "./a.ts"; // error
12+
import { type A as _A } from "./a.ts"; // error
13+
type __A = import("./a.ts").A; // ok
14+
const aPromise = import("./a.ts"); // error
15+
16+
//// [c.ts]
17+
import type { A } from "./a.d.ts"; // ok
18+
import {} from "./a.d.ts"; // error
19+
import { type A as _A } from "./a.d.ts"; // error
20+
type __A = import("./a.d.ts").A; // ok
21+
const aPromise = import("./a.d.ts"); // error
22+
23+
24+
//// [a.js]
25+
export class A {
26+
}
27+
//// [b.js]
28+
const aPromise = import("./a.ts"); // error
29+
export {};
30+
//// [c.js]
31+
const aPromise = import("./a.d.ts"); // error
32+
export {};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//// [tests/cases/conformance/externalModules/typeOnly/allowsImportingTsExtension.ts] ////
2+
3+
=== a.ts ===
4+
export class A {}
5+
>A : Symbol(A, Decl(a.ts, 0, 0))
6+
7+
=== a.d.ts ===
8+
export class A {}
9+
>A : Symbol(A, Decl(a.d.ts, 0, 0))
10+
11+
=== b.ts ===
12+
import type { A } from "./a.ts"; // ok
13+
>A : Symbol(A, Decl(b.ts, 0, 13))
14+
15+
import {} from "./a.ts"; // error
16+
import { type A as _A } from "./a.ts"; // error
17+
>A : Symbol(A, Decl(a.ts, 0, 0))
18+
>_A : Symbol(_A, Decl(b.ts, 2, 8))
19+
20+
type __A = import("./a.ts").A; // ok
21+
>__A : Symbol(__A, Decl(b.ts, 2, 38))
22+
>A : Symbol(A, Decl(a.ts, 0, 0))
23+
24+
const aPromise = import("./a.ts"); // error
25+
>aPromise : Symbol(aPromise, Decl(b.ts, 4, 5))
26+
>"./a.ts" : Symbol("a", Decl(a.ts, 0, 0))
27+
28+
=== c.ts ===
29+
import type { A } from "./a.d.ts"; // ok
30+
>A : Symbol(A, Decl(c.ts, 0, 13))
31+
32+
import {} from "./a.d.ts"; // error
33+
import { type A as _A } from "./a.d.ts"; // error
34+
>A : Symbol(A, Decl(a.ts, 0, 0))
35+
>_A : Symbol(_A, Decl(c.ts, 2, 8))
36+
37+
type __A = import("./a.d.ts").A; // ok
38+
>__A : Symbol(__A, Decl(c.ts, 2, 40))
39+
>A : Symbol(A, Decl(a.ts, 0, 0))
40+
41+
const aPromise = import("./a.d.ts"); // error
42+
>aPromise : Symbol(aPromise, Decl(c.ts, 4, 5))
43+
>"./a.d.ts" : Symbol("a", Decl(a.ts, 0, 0))
44+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//// [tests/cases/conformance/externalModules/typeOnly/allowsImportingTsExtension.ts] ////
2+
3+
=== a.ts ===
4+
export class A {}
5+
>A : A
6+
7+
=== a.d.ts ===
8+
export class A {}
9+
>A : A
10+
11+
=== b.ts ===
12+
import type { A } from "./a.ts"; // ok
13+
>A : A
14+
15+
import {} from "./a.ts"; // error
16+
import { type A as _A } from "./a.ts"; // error
17+
>A : typeof A
18+
>_A : typeof A
19+
20+
type __A = import("./a.ts").A; // ok
21+
>__A : A
22+
23+
const aPromise = import("./a.ts"); // error
24+
>aPromise : Promise<typeof import("a")>
25+
>import("./a.ts") : Promise<typeof import("a")>
26+
>"./a.ts" : "./a.ts"
27+
28+
=== c.ts ===
29+
import type { A } from "./a.d.ts"; // ok
30+
>A : A
31+
32+
import {} from "./a.d.ts"; // error
33+
import { type A as _A } from "./a.d.ts"; // error
34+
>A : typeof A
35+
>_A : typeof A
36+
37+
type __A = import("./a.d.ts").A; // ok
38+
>__A : A
39+
40+
const aPromise = import("./a.d.ts"); // error
41+
>aPromise : Promise<typeof import("a")>
42+
>import("./a.d.ts") : Promise<typeof import("a")>
43+
>"./a.d.ts" : "./a.d.ts"
44+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// @allowImportingTsExtensions: false
2+
// @target: esnext
3+
// @module: esnext
4+
5+
// @Filename: a.ts
6+
export class A {}
7+
8+
// @Filename: a.d.ts
9+
export class A {}
10+
11+
// @Filename: b.ts
12+
import type { A } from "./a.ts"; // ok
13+
import {} from "./a.ts"; // error
14+
import { type A as _A } from "./a.ts"; // error
15+
type __A = import("./a.ts").A; // ok
16+
const aPromise = import("./a.ts"); // error
17+
18+
// @Filename: c.ts
19+
import type { A } from "./a.d.ts"; // ok
20+
import {} from "./a.d.ts"; // error
21+
import { type A as _A } from "./a.d.ts"; // error
22+
type __A = import("./a.d.ts").A; // ok
23+
const aPromise = import("./a.d.ts"); // error
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/// <reference path="fourslash.ts" />
2+
// @module: nodenext
3+
// @allowImportingTsExtensions: false
4+
5+
// @Filename: /exports.ts
6+
//// export interface SomeInterface {}
7+
//// export class SomePig {}
8+
9+
// @Filename: /a.ts
10+
//// import type { SomePig } from "./exports.ts";
11+
//// new SomePig/**/
12+
13+
verify.completions({
14+
marker: "",
15+
includes: [{
16+
name: "SomePig",
17+
source: completion.CompletionSource.TypeOnlyAlias,
18+
hasAction: true,
19+
}]
20+
});
21+
22+
verify.applyCodeActionFromCompletion("", {
23+
name: "SomePig",
24+
source: completion.CompletionSource.TypeOnlyAlias,
25+
description: `Remove 'type' from import declaration from "./exports.ts"`,
26+
newFileContent:
27+
`import { SomePig } from "./exports.js";
28+
new SomePig`,
29+
preferences: {
30+
includeCompletionsForModuleExports: true,
31+
allowIncompleteCompletions: true,
32+
includeInsertTextCompletions: true,
33+
},
34+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/// <reference path="fourslash.ts" />
2+
// @module: nodenext
3+
// @allowImportingTsExtensions: true
4+
5+
// @Filename: /exports.ts
6+
//// export interface SomeInterface {}
7+
//// export class SomePig {}
8+
9+
// @Filename: /a.ts
10+
//// import type { SomePig } from "./exports.ts";
11+
//// new SomePig/**/
12+
13+
verify.completions({
14+
marker: "",
15+
includes: [{
16+
name: "SomePig",
17+
source: completion.CompletionSource.TypeOnlyAlias,
18+
hasAction: true,
19+
}]
20+
});
21+
22+
verify.applyCodeActionFromCompletion("", {
23+
name: "SomePig",
24+
source: completion.CompletionSource.TypeOnlyAlias,
25+
description: `Remove 'type' from import declaration from "./exports.ts"`,
26+
newFileContent:
27+
`import { SomePig } from "./exports.ts";
28+
new SomePig`,
29+
preferences: {
30+
includeCompletionsForModuleExports: true,
31+
allowIncompleteCompletions: true,
32+
includeInsertTextCompletions: true,
33+
},
34+
});

0 commit comments

Comments
 (0)