Skip to content

Commit 3b396e6

Browse files
Type-only imports and exports (#35200)
* Add type-only support for export declarations * Use a synthetic type alias instead of binding type-only exports as a type alias * Works for re-exports! * isolatedModules works fine * Diagnostic for type-only exporting a value * Start isolated modules codefix * Update for LKG control flow changes * Type-only import clause parsing * Type-only default import checking * Type-only named imports * Fix isolated modules error * Filter namespaces down to type-only * Fix class references * Test nested namespaces * Test circular type-only imports/exports * Fix getTypeAtLocation for type-only import/export specifiers * Fix type-only generic imports * Update public APIs * Remove unused WIP comment * Type-only namespace imports * Fix factory update calls * Add grammar errors for JS usage and mixing default and named bindings * Update updateExportDeclaration API baseline * Fix grammar checking import clauses * Enums, sort of * Dedicated error for type-only enum * Skip past type-only alias symbols in quick info * Update error code in baseline * WIP: convertToTypeOnlyExport * isolatedModules codefix (single export declaration) * isolatedModules code fix (all) * Stop eliding non-type-only imports by default, add compiler flag * Update to match updated diagnostic messages * Update more baselines * Update more tests * Auto-import as type-only * Add codefix for splitting type-only import with default and named bindings * Add more services tests * Add targeted error message for "export type T;" when T exists * Add targeted error for "import type T = require(...)" * Flip emit flag * Add test for preserveUnusedImports option * Fix flag flip on import = * Make compiler option string-valued * Fix merge conflicts * Add --importsNotUsedAsValue=error * Phrasing of messages. Co-authored-by: Daniel Rosenwasser <[email protected]>
1 parent 18269c0 commit 3b396e6

File tree

142 files changed

+3298
-121
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

142 files changed

+3298
-121
lines changed

src/compiler/binder.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,8 +517,13 @@ namespace ts {
517517
}
518518
}
519519

520-
const declarationName = getNameOfDeclaration(node) || node;
521520
const relatedInformation: DiagnosticRelatedInformation[] = [];
521+
if (isTypeAliasDeclaration(node) && nodeIsMissing(node.type) && hasModifier(node, ModifierFlags.Export) && symbol.flags & (SymbolFlags.Alias | SymbolFlags.Type | SymbolFlags.Namespace)) {
522+
// export type T; - may have meant export type { T }?
523+
relatedInformation.push(createDiagnosticForNode(node, Diagnostics.Did_you_mean_0, `export type { ${unescapeLeadingUnderscores(node.name.escapedText)} }`));
524+
}
525+
526+
const declarationName = getNameOfDeclaration(node) || node;
522527
forEach(symbol.declarations, (declaration, index) => {
523528
const decl = getNameOfDeclaration(declaration) || declaration;
524529
const diag = createDiagnosticForNode(decl, message, messageNeedsName ? getDisplayName(declaration) : undefined);
@@ -531,7 +536,7 @@ namespace ts {
531536
});
532537

533538
const diag = createDiagnosticForNode(declarationName, message, messageNeedsName ? getDisplayName(node) : undefined);
534-
file.bindDiagnostics.push(multipleDefaultExports ? addRelatedInfo(diag, ...relatedInformation) : diag);
539+
file.bindDiagnostics.push(addRelatedInfo(diag, ...relatedInformation));
535540

536541
symbol = createSymbol(SymbolFlags.None, name);
537542
}

src/compiler/checker.ts

Lines changed: 178 additions & 22 deletions
Large diffs are not rendered by default.

src/compiler/commandLineParser.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,17 @@ namespace ts {
472472
category: Diagnostics.Basic_Options,
473473
description: Diagnostics.Import_emit_helpers_from_tslib
474474
},
475+
{
476+
name: "importsNotUsedAsValue",
477+
type: createMapFromTemplate({
478+
remove: ImportsNotUsedAsValue.Remove,
479+
preserve: ImportsNotUsedAsValue.Preserve,
480+
error: ImportsNotUsedAsValue.Error
481+
}),
482+
affectsEmit: true,
483+
category: Diagnostics.Advanced_Options,
484+
description: Diagnostics.Specify_emit_Slashchecking_behavior_for_imports_that_are_only_used_for_types
485+
},
475486
{
476487
name: "downlevelIteration",
477488
type: "boolean",

src/compiler/core.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,13 @@ namespace ts {
15091509
return compareComparableValues(a, b);
15101510
}
15111511

1512+
/**
1513+
* Compare two TextSpans, first by `start`, then by `length`.
1514+
*/
1515+
export function compareTextSpans(a: Partial<TextSpan> | undefined, b: Partial<TextSpan> | undefined): Comparison {
1516+
return compareValues(a?.start, b?.start) || compareValues(a?.length, b?.length);
1517+
}
1518+
15121519
export function min<T>(a: T, b: T, compare: Comparer<T>): T {
15131520
return compare(a, b) === Comparison.LessThan ? a : b;
15141521
}
@@ -1914,10 +1921,10 @@ namespace ts {
19141921
return (arg: T) => f(arg) && g(arg);
19151922
}
19161923

1917-
export function or<T extends unknown>(...fs: ((arg: T) => boolean)[]): (arg: T) => boolean {
1918-
return arg => {
1924+
export function or<T extends unknown[]>(...fs: ((...args: T) => boolean)[]): (...args: T) => boolean {
1925+
return (...args) => {
19191926
for (const f of fs) {
1920-
if (f(arg)) {
1927+
if (f(...args)) {
19211928
return true;
19221929
}
19231930
}

src/compiler/diagnosticMessages.json

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,7 @@
639639
"category": "Error",
640640
"code": 1203
641641
},
642-
"Cannot re-export a type when the '--isolatedModules' flag is provided.": {
642+
"Re-exporting a type when the '--isolatedModules' flag is provided requires using 'export type'.": {
643643
"category": "Error",
644644
"code": 1205
645645
},
@@ -1059,10 +1059,66 @@
10591059
"category": "Error",
10601060
"code": 1360
10611061
},
1062-
"'await' outside of an async function is only allowed at the top level of a module when '--module' is 'esnext' or 'system' and '--target' is 'es2017' or higher.": {
1062+
"Type-only {0} must reference a type, but '{1}' is a value.": {
10631063
"category": "Error",
10641064
"code": 1361
10651065
},
1066+
"Enum '{0}' cannot be used as a value because only its type has been imported.": {
1067+
"category": "Error",
1068+
"code": 1362
1069+
},
1070+
"A type-only import can specify a default import or named bindings, but not both.": {
1071+
"category": "Error",
1072+
"code": 1363
1073+
},
1074+
"Convert to type-only export": {
1075+
"category": "Message",
1076+
"code": 1364
1077+
},
1078+
"Convert all re-exported types to type-only exports": {
1079+
"category": "Message",
1080+
"code": 1365
1081+
},
1082+
"Split into two separate import declarations": {
1083+
"category": "Message",
1084+
"code": 1366
1085+
},
1086+
"Split all invalid type-only imports": {
1087+
"category": "Message",
1088+
"code": 1377
1089+
},
1090+
"Specify emit/checking behavior for imports that are only used for types": {
1091+
"category": "Message",
1092+
"code": 1368
1093+
},
1094+
"Did you mean '{0}'?": {
1095+
"category": "Message",
1096+
"code": 1369
1097+
},
1098+
"Only ECMAScript imports may use 'import type'.": {
1099+
"category": "Error",
1100+
"code": 1370
1101+
},
1102+
"This import is never used as a value and must use 'import type' because the 'importsNotUsedAsValue' is set to 'error'.": {
1103+
"category": "Error",
1104+
"code": 1371
1105+
},
1106+
"This import may be converted to a type-only import.": {
1107+
"category": "Suggestion",
1108+
"code": 1372
1109+
},
1110+
"Convert to type-only import": {
1111+
"category": "Message",
1112+
"code": 1373
1113+
},
1114+
"Convert all imports not used as a value to type-only imports": {
1115+
"category": "Message",
1116+
"code": 1374
1117+
},
1118+
"'await' outside of an async function is only allowed at the top level of a module when '--module' is 'esnext' or 'system' and '--target' is 'es2017' or higher.": {
1119+
"category": "Error",
1120+
"code": 1375
1121+
},
10661122

10671123
"The types of '{0}' are incompatible between these types.": {
10681124
"category": "Error",

src/compiler/emitter.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3048,6 +3048,10 @@ namespace ts {
30483048
}
30493049

30503050
function emitImportClause(node: ImportClause) {
3051+
if (node.isTypeOnly) {
3052+
emitTokenWithComment(SyntaxKind.TypeKeyword, node.pos, writeKeyword, node);
3053+
writeSpace();
3054+
}
30513055
emit(node.name);
30523056
if (node.name && node.namedBindings) {
30533057
emitTokenWithComment(SyntaxKind.CommaToken, node.name.end, writePunctuation, node);
@@ -3089,6 +3093,10 @@ namespace ts {
30893093
function emitExportDeclaration(node: ExportDeclaration) {
30903094
let nextPos = emitTokenWithComment(SyntaxKind.ExportKeyword, node.pos, writeKeyword, node);
30913095
writeSpace();
3096+
if (node.isTypeOnly) {
3097+
nextPos = emitTokenWithComment(SyntaxKind.TypeKeyword, nextPos, writeKeyword, node);
3098+
writeSpace();
3099+
}
30923100
if (node.exportClause) {
30933101
emit(node.exportClause);
30943102
}

src/compiler/factoryPublic.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2267,17 +2267,19 @@ namespace ts {
22672267
: node;
22682268
}
22692269

2270-
export function createImportClause(name: Identifier | undefined, namedBindings: NamedImportBindings | undefined): ImportClause {
2270+
export function createImportClause(name: Identifier | undefined, namedBindings: NamedImportBindings | undefined, isTypeOnly = false): ImportClause {
22712271
const node = <ImportClause>createSynthesizedNode(SyntaxKind.ImportClause);
22722272
node.name = name;
22732273
node.namedBindings = namedBindings;
2274+
node.isTypeOnly = isTypeOnly;
22742275
return node;
22752276
}
22762277

2277-
export function updateImportClause(node: ImportClause, name: Identifier | undefined, namedBindings: NamedImportBindings | undefined) {
2278+
export function updateImportClause(node: ImportClause, name: Identifier | undefined, namedBindings: NamedImportBindings | undefined, isTypeOnly: boolean) {
22782279
return node.name !== name
22792280
|| node.namedBindings !== namedBindings
2280-
? updateNode(createImportClause(name, namedBindings), node)
2281+
|| node.isTypeOnly !== isTypeOnly
2282+
? updateNode(createImportClause(name, namedBindings, isTypeOnly), node)
22812283
: node;
22822284
}
22832285

@@ -2348,10 +2350,11 @@ namespace ts {
23482350
: node;
23492351
}
23502352

2351-
export function createExportDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, exportClause: NamedExportBindings | undefined, moduleSpecifier?: Expression) {
2353+
export function createExportDeclaration(decorators: readonly Decorator[] | undefined, modifiers: readonly Modifier[] | undefined, exportClause: NamedExportBindings | undefined, moduleSpecifier?: Expression, isTypeOnly = false) {
23522354
const node = <ExportDeclaration>createSynthesizedNode(SyntaxKind.ExportDeclaration);
23532355
node.decorators = asNodeArray(decorators);
23542356
node.modifiers = asNodeArray(modifiers);
2357+
node.isTypeOnly = isTypeOnly;
23552358
node.exportClause = exportClause;
23562359
node.moduleSpecifier = moduleSpecifier;
23572360
return node;
@@ -2362,12 +2365,14 @@ namespace ts {
23622365
decorators: readonly Decorator[] | undefined,
23632366
modifiers: readonly Modifier[] | undefined,
23642367
exportClause: NamedExportBindings | undefined,
2365-
moduleSpecifier: Expression | undefined) {
2368+
moduleSpecifier: Expression | undefined,
2369+
isTypeOnly: boolean) {
23662370
return node.decorators !== decorators
23672371
|| node.modifiers !== modifiers
2372+
|| node.isTypeOnly !== isTypeOnly
23682373
|| node.exportClause !== exportClause
23692374
|| node.moduleSpecifier !== moduleSpecifier
2370-
? updateNode(createExportDeclaration(decorators, modifiers, exportClause, moduleSpecifier), node)
2375+
? updateNode(createExportDeclaration(decorators, modifiers, exportClause, moduleSpecifier, isTypeOnly), node)
23712376
: node;
23722377
}
23732378

src/compiler/parser.ts

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,7 +1516,10 @@ namespace ts {
15161516
if (token() === SyntaxKind.DefaultKeyword) {
15171517
return lookAhead(nextTokenCanFollowDefaultKeyword);
15181518
}
1519-
return token() !== SyntaxKind.AsteriskToken && token() !== SyntaxKind.AsKeyword && token() !== SyntaxKind.OpenBraceToken && canFollowModifier();
1519+
if (token() === SyntaxKind.TypeKeyword) {
1520+
return lookAhead(nextTokenCanFollowExportModifier);
1521+
}
1522+
return canFollowExportModifier();
15201523
case SyntaxKind.DefaultKeyword:
15211524
return nextTokenCanFollowDefaultKeyword();
15221525
case SyntaxKind.StaticKeyword:
@@ -1529,6 +1532,18 @@ namespace ts {
15291532
}
15301533
}
15311534

1535+
function canFollowExportModifier(): boolean {
1536+
return token() !== SyntaxKind.AsteriskToken
1537+
&& token() !== SyntaxKind.AsKeyword
1538+
&& token() !== SyntaxKind.OpenBraceToken
1539+
&& canFollowModifier();
1540+
}
1541+
1542+
function nextTokenCanFollowExportModifier(): boolean {
1543+
nextToken();
1544+
return canFollowExportModifier();
1545+
}
1546+
15321547
function parseAnyContextualModifier(): boolean {
15331548
return isModifierKind(token()) && tryParse(nextTokenCanFollowModifier);
15341549
}
@@ -5470,10 +5485,13 @@ namespace ts {
54705485
return token() === SyntaxKind.StringLiteral || token() === SyntaxKind.AsteriskToken ||
54715486
token() === SyntaxKind.OpenBraceToken || tokenIsIdentifierOrKeyword(token());
54725487
case SyntaxKind.ExportKeyword:
5473-
nextToken();
5474-
if (token() === SyntaxKind.EqualsToken || token() === SyntaxKind.AsteriskToken ||
5475-
token() === SyntaxKind.OpenBraceToken || token() === SyntaxKind.DefaultKeyword ||
5476-
token() === SyntaxKind.AsKeyword) {
5488+
let currentToken = nextToken();
5489+
if (currentToken === SyntaxKind.TypeKeyword) {
5490+
currentToken = lookAhead(nextToken);
5491+
}
5492+
if (currentToken === SyntaxKind.EqualsToken || currentToken === SyntaxKind.AsteriskToken ||
5493+
currentToken === SyntaxKind.OpenBraceToken || currentToken === SyntaxKind.DefaultKeyword ||
5494+
currentToken === SyntaxKind.AsKeyword) {
54775495
return true;
54785496
}
54795497
continue;
@@ -6355,9 +6373,19 @@ namespace ts {
63556373
let identifier: Identifier | undefined;
63566374
if (isIdentifier()) {
63576375
identifier = parseIdentifier();
6358-
if (token() !== SyntaxKind.CommaToken && token() !== SyntaxKind.FromKeyword) {
6359-
return parseImportEqualsDeclaration(<ImportEqualsDeclaration>node, identifier);
6360-
}
6376+
}
6377+
6378+
let isTypeOnly = false;
6379+
if (token() !== SyntaxKind.FromKeyword &&
6380+
identifier?.escapedText === "type" &&
6381+
(isIdentifier() || tokenAfterImportDefinitelyProducesImportDeclaration())
6382+
) {
6383+
isTypeOnly = true;
6384+
identifier = isIdentifier() ? parseIdentifier() : undefined;
6385+
}
6386+
6387+
if (identifier && !tokenAfterImportedIdentifierDefinitelyProducesImportDeclaration()) {
6388+
return parseImportEqualsDeclaration(<ImportEqualsDeclaration>node, identifier, isTypeOnly);
63616389
}
63626390

63636391
// Import statement
@@ -6366,9 +6394,10 @@ namespace ts {
63666394
// import ImportClause from ModuleSpecifier ;
63676395
// import ModuleSpecifier;
63686396
if (identifier || // import id
6369-
token() === SyntaxKind.AsteriskToken || // import *
6370-
token() === SyntaxKind.OpenBraceToken) { // import {
6371-
(<ImportDeclaration>node).importClause = parseImportClause(identifier, afterImportPos);
6397+
token() === SyntaxKind.AsteriskToken || // import *
6398+
token() === SyntaxKind.OpenBraceToken // import {
6399+
) {
6400+
(<ImportDeclaration>node).importClause = parseImportClause(identifier, afterImportPos, isTypeOnly);
63726401
parseExpected(SyntaxKind.FromKeyword);
63736402
}
63746403

@@ -6377,16 +6406,30 @@ namespace ts {
63776406
return finishNode(node);
63786407
}
63796408

6380-
function parseImportEqualsDeclaration(node: ImportEqualsDeclaration, identifier: Identifier): ImportEqualsDeclaration {
6409+
function tokenAfterImportDefinitelyProducesImportDeclaration() {
6410+
return token() === SyntaxKind.AsteriskToken || token() === SyntaxKind.OpenBraceToken;
6411+
}
6412+
6413+
function tokenAfterImportedIdentifierDefinitelyProducesImportDeclaration() {
6414+
// In `import id ___`, the current token decides whether to produce
6415+
// an ImportDeclaration or ImportEqualsDeclaration.
6416+
return token() === SyntaxKind.CommaToken || token() === SyntaxKind.FromKeyword;
6417+
}
6418+
6419+
function parseImportEqualsDeclaration(node: ImportEqualsDeclaration, identifier: Identifier, isTypeOnly: boolean): ImportEqualsDeclaration {
63816420
node.kind = SyntaxKind.ImportEqualsDeclaration;
63826421
node.name = identifier;
63836422
parseExpected(SyntaxKind.EqualsToken);
63846423
node.moduleReference = parseModuleReference();
63856424
parseSemicolon();
6386-
return finishNode(node);
6425+
const finished = finishNode(node);
6426+
if (isTypeOnly) {
6427+
parseErrorAtRange(finished, Diagnostics.Only_ECMAScript_imports_may_use_import_type);
6428+
}
6429+
return finished;
63876430
}
63886431

6389-
function parseImportClause(identifier: Identifier | undefined, fullStart: number) {
6432+
function parseImportClause(identifier: Identifier | undefined, fullStart: number, isTypeOnly: boolean) {
63906433
// ImportClause:
63916434
// ImportedDefaultBinding
63926435
// NameSpaceImport
@@ -6395,6 +6438,8 @@ namespace ts {
63956438
// ImportedDefaultBinding, NamedImports
63966439

63976440
const importClause = <ImportClause>createNode(SyntaxKind.ImportClause, fullStart);
6441+
importClause.isTypeOnly = isTypeOnly;
6442+
63986443
if (identifier) {
63996444
// ImportedDefaultBinding:
64006445
// ImportedBinding
@@ -6514,6 +6559,7 @@ namespace ts {
65146559

65156560
function parseExportDeclaration(node: ExportDeclaration): ExportDeclaration {
65166561
node.kind = SyntaxKind.ExportDeclaration;
6562+
node.isTypeOnly = parseOptional(SyntaxKind.TypeKeyword);
65176563
if (parseOptional(SyntaxKind.AsteriskToken)) {
65186564
if (parseOptional(SyntaxKind.AsKeyword)) {
65196565
node.exportClause = parseNamespaceExport();

src/compiler/program.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,6 +1820,18 @@ namespace ts {
18201820
}
18211821

18221822
switch (node.kind) {
1823+
case SyntaxKind.ImportClause:
1824+
if ((node as ImportClause).isTypeOnly) {
1825+
diagnostics.push(createDiagnosticForNode(node.parent, Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, "import type"));
1826+
return;
1827+
}
1828+
break;
1829+
case SyntaxKind.ExportDeclaration:
1830+
if ((node as ExportDeclaration).isTypeOnly) {
1831+
diagnostics.push(createDiagnosticForNode(node, Diagnostics._0_declarations_can_only_be_used_in_TypeScript_files, "export type"));
1832+
return;
1833+
}
1834+
break;
18231835
case SyntaxKind.ImportEqualsDeclaration:
18241836
diagnostics.push(createDiagnosticForNode(node, Diagnostics.import_can_only_be_used_in_TypeScript_files));
18251837
return;

0 commit comments

Comments
 (0)