diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index d336627667a0e..214b43aaba465 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -37650,7 +37650,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
error(node, Diagnostics.The_import_meta_meta_property_is_only_allowed_when_the_module_option_is_es2020_es2022_esnext_system_node16_node18_or_nodenext);
}
const file = getSourceFileOfNode(node);
- Debug.assert(!!(file.flags & NodeFlags.PossiblyContainsImportMeta), "Containing file is missing import meta node flag.");
+ Debug.assert(node.name.escapedText === "defer" || !!(file.flags & NodeFlags.PossiblyContainsImportMeta), "Containing file is missing import meta node flag.");
return node.name.escapedText === "meta" ? getGlobalImportMetaType() : errorType;
}
@@ -41172,8 +41172,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
case SyntaxKind.ElementAccessExpression:
return checkIndexedAccess(node as ElementAccessExpression, checkMode);
case SyntaxKind.CallExpression:
- if ((node as CallExpression).expression.kind === SyntaxKind.ImportKeyword) {
- return checkImportCallExpression(node as ImportCall);
+ if (isImportCall(node)) {
+ return checkImportCallExpression(node);
}
// falls through
case SyntaxKind.NewExpression:
@@ -52612,8 +52612,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
break;
case SyntaxKind.ImportKeyword:
- if (escapedText !== "meta") {
- return grammarErrorOnNode(node.name, Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, unescapeLeadingUnderscores(node.name.escapedText), tokenToString(node.keywordToken), "meta");
+ if (escapedText !== "meta" && escapedText !== "defer") {
+ const suggestion = node.parent.kind === SyntaxKind.CallExpression && (node.parent as CallExpression).expression === node
+ ? "meta' or 'defer"
+ : "meta";
+ return grammarErrorOnNode(node.name, Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, unescapeLeadingUnderscores(node.name.escapedText), tokenToString(node.keywordToken), suggestion);
}
break;
}
@@ -52902,6 +52905,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return grammarErrorOnNode(node, Diagnostics.ESM_syntax_is_not_allowed_in_a_CommonJS_module_when_verbatimModuleSyntax_is_enabled);
}
+ if (node.expression.kind === SyntaxKind.MetaProperty) {
+ if (moduleKind !== ModuleKind.ESNext) {
+ return grammarErrorOnNode(node, Diagnostics.Deferred_imports_are_only_supported_when_the_module_flag_is_set_to_esnext);
+ }
+ }
if (moduleKind === ModuleKind.ES2015) {
return grammarErrorOnNode(node, Diagnostics.Dynamic_imports_are_only_supported_when_the_module_flag_is_set_to_es2020_es2022_esnext_commonjs_amd_system_umd_node16_node18_or_nodenext);
}
diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json
index cfc166c130fdb..e8fcb5123e809 100644
--- a/src/compiler/diagnosticMessages.json
+++ b/src/compiler/diagnosticMessages.json
@@ -8445,5 +8445,9 @@
"Deferred imports are only supported when the '--module' flag is set to 'esnext'.": {
"category": "Error",
"code": 18060
+ },
+ "'import.defer' must be followed by '('": {
+ "category": "Error",
+ "code": 18061
}
}
diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts
index 44fa145fbc293..8c204ebb761b5 100644
--- a/src/compiler/parser.ts
+++ b/src/compiler/parser.ts
@@ -5939,7 +5939,18 @@ namespace Parser {
nextToken(); // advance past the 'import'
nextToken(); // advance past the dot
expression = finishNode(factory.createMetaProperty(SyntaxKind.ImportKeyword, parseIdentifierName()), pos);
- sourceFlags |= NodeFlags.PossiblyContainsImportMeta;
+
+ if ((expression as MetaProperty).name.escapedText === "defer") {
+ if (token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken) {
+ sourceFlags |= NodeFlags.PossiblyContainsDynamicImport;
+ }
+ else {
+ parseErrorAt(pos, getNodePos(), Diagnostics.import_defer_must_be_followed_by);
+ }
+ }
+ else {
+ sourceFlags |= NodeFlags.PossiblyContainsImportMeta;
+ }
}
else {
expression = parseMemberExpressionOrHigher();
diff --git a/src/compiler/types.ts b/src/compiler/types.ts
index dce42356655b8..05617b1cc782b 100644
--- a/src/compiler/types.ts
+++ b/src/compiler/types.ts
@@ -3101,7 +3101,7 @@ export interface SuperCall extends CallExpression {
}
export interface ImportCall extends CallExpression {
- readonly expression: ImportExpression;
+ readonly expression: ImportExpression | ImportDeferProperty;
}
export interface ExpressionWithTypeArguments extends MemberExpression, NodeWithTypeArguments {
@@ -3181,6 +3181,12 @@ export interface ImportMetaProperty extends MetaProperty {
readonly name: Identifier & { readonly escapedText: __String & "meta"; };
}
+/** @internal */
+export interface ImportDeferProperty extends MetaProperty {
+ readonly keywordToken: SyntaxKind.ImportKeyword;
+ readonly name: Identifier & { readonly escapedText: __String & "defer"; };
+}
+
/// A JSX expression of the form ...
export interface JsxElement extends PrimaryExpression {
readonly kind: SyntaxKind.JsxElement;
diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts
index 287f04829bc1a..6c65fb2005d1c 100644
--- a/src/compiler/utilities.ts
+++ b/src/compiler/utilities.ts
@@ -2632,7 +2632,13 @@ export function isSuperCall(n: Node): n is SuperCall {
/** @internal */
export function isImportCall(n: Node): n is ImportCall {
- return n.kind === SyntaxKind.CallExpression && (n as CallExpression).expression.kind === SyntaxKind.ImportKeyword;
+ if (n.kind !== SyntaxKind.CallExpression) return false;
+ const e = (n as CallExpression).expression;
+ return e.kind === SyntaxKind.ImportKeyword || (
+ isMetaProperty(e)
+ && e.keywordToken === SyntaxKind.ImportKeyword
+ && e.name.escapedText === "defer"
+ );
}
/** @internal */
diff --git a/tests/baselines/reference/dynamicImportDefer.errors.txt b/tests/baselines/reference/dynamicImportDefer.errors.txt
new file mode 100644
index 0000000000000..a67ed059cb6f6
--- /dev/null
+++ b/tests/baselines/reference/dynamicImportDefer.errors.txt
@@ -0,0 +1,20 @@
+error TS2468: Cannot find global value 'Promise'.
+b.ts(1,1): error TS2712: A dynamic import call in ES5 requires the 'Promise' constructor. Make sure you have a declaration for the 'Promise' constructor or include 'ES2015' in your '--lib' option.
+b.ts(1,14): error TS2792: Cannot find module 'a'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option?
+
+
+!!! error TS2468: Cannot find global value 'Promise'.
+==== a.ts (0 errors) ====
+ export function foo() {
+ console.log("foo from a");
+ }
+
+==== b.ts (2 errors) ====
+ import.defer("a").then(ns => {
+ ~~~~~~~~~~~~~~~~~
+!!! error TS2712: A dynamic import call in ES5 requires the 'Promise' constructor. Make sure you have a declaration for the 'Promise' constructor or include 'ES2015' in your '--lib' option.
+ ~~~
+!!! error TS2792: Cannot find module 'a'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option?
+ ns.foo();
+ });
+
\ No newline at end of file
diff --git a/tests/baselines/reference/dynamicImportDefer.js b/tests/baselines/reference/dynamicImportDefer.js
new file mode 100644
index 0000000000000..0b7832a55d6c5
--- /dev/null
+++ b/tests/baselines/reference/dynamicImportDefer.js
@@ -0,0 +1,21 @@
+//// [tests/cases/conformance/importDefer/dynamicImportDefer.ts] ////
+
+//// [a.ts]
+export function foo() {
+ console.log("foo from a");
+}
+
+//// [b.ts]
+import.defer("a").then(ns => {
+ ns.foo();
+});
+
+
+//// [a.js]
+export function foo() {
+ console.log("foo from a");
+}
+//// [b.js]
+import.defer("a").then(function (ns) {
+ ns.foo();
+});
diff --git a/tests/baselines/reference/dynamicImportDefer.symbols b/tests/baselines/reference/dynamicImportDefer.symbols
new file mode 100644
index 0000000000000..b255c5fba2f6f
--- /dev/null
+++ b/tests/baselines/reference/dynamicImportDefer.symbols
@@ -0,0 +1,23 @@
+//// [tests/cases/conformance/importDefer/dynamicImportDefer.ts] ////
+
+=== a.ts ===
+export function foo() {
+>foo : Symbol(foo, Decl(a.ts, 0, 0))
+
+ console.log("foo from a");
+>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
+>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
+>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
+}
+
+=== b.ts ===
+import.defer("a").then(ns => {
+>import.defer("a").then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --))
+>then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --))
+>ns : Symbol(ns, Decl(b.ts, 0, 23))
+
+ ns.foo();
+>ns : Symbol(ns, Decl(b.ts, 0, 23))
+
+});
+
diff --git a/tests/baselines/reference/dynamicImportDefer.types b/tests/baselines/reference/dynamicImportDefer.types
new file mode 100644
index 0000000000000..05ecaa37b5a7c
--- /dev/null
+++ b/tests/baselines/reference/dynamicImportDefer.types
@@ -0,0 +1,53 @@
+//// [tests/cases/conformance/importDefer/dynamicImportDefer.ts] ////
+
+=== a.ts ===
+export function foo() {
+>foo : () => void
+> : ^^^^^^^^^^
+
+ console.log("foo from a");
+>console.log("foo from a") : void
+> : ^^^^
+>console.log : (...data: any[]) => void
+> : ^^^^ ^^ ^^^^^
+>console : Console
+> : ^^^^^^^
+>log : (...data: any[]) => void
+> : ^^^^ ^^ ^^^^^
+>"foo from a" : "foo from a"
+> : ^^^^^^^^^^^^
+}
+
+=== b.ts ===
+import.defer("a").then(ns => {
+>import.defer("a").then(ns => { ns.foo();}) : Promise
+> : ^^^^^^^^^^^^^
+>import.defer("a").then : (onfulfilled?: (value: any) => TResult1 | PromiseLike, onrejected?: (reason: any) => TResult2 | PromiseLike) => Promise
+> : ^ ^^^^^^^^ ^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+>import.defer("a") : Promise
+> : ^^^^^^^^^^^^
+>import.defer : any
+> : ^^^
+>defer : any
+> : ^^^
+>"a" : "a"
+> : ^^^
+>then : (onfulfilled?: (value: any) => TResult1 | PromiseLike, onrejected?: (reason: any) => TResult2 | PromiseLike) => Promise
+> : ^ ^^^^^^^^ ^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+>ns => { ns.foo();} : (ns: any) => void
+> : ^ ^^^^^^^^^^^^^^
+>ns : any
+> : ^^^
+
+ ns.foo();
+>ns.foo() : any
+> : ^^^
+>ns.foo : any
+> : ^^^
+>ns : any
+> : ^^^
+>foo : any
+> : ^^^
+
+});
+
diff --git a/tests/baselines/reference/dynamicImportDeferInvalidStandalone.errors.txt b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.errors.txt
new file mode 100644
index 0000000000000..69d00e1709ae2
--- /dev/null
+++ b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.errors.txt
@@ -0,0 +1,18 @@
+b.ts(1,1): error TS18061: 'import.defer' must be followed by '('
+b.ts(3,2): error TS18061: 'import.defer' must be followed by '('
+
+
+==== a.ts (0 errors) ====
+ export function foo() {
+ console.log("foo from a");
+ }
+
+==== b.ts (2 errors) ====
+ import.defer;
+ ~~~~~~~~~~~~
+!!! error TS18061: 'import.defer' must be followed by '('
+
+ (import.defer)("a");
+ ~~~~~~~~~~~~
+!!! error TS18061: 'import.defer' must be followed by '('
+
\ No newline at end of file
diff --git a/tests/baselines/reference/dynamicImportDeferInvalidStandalone.js b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.js
new file mode 100644
index 0000000000000..60c43006b5c42
--- /dev/null
+++ b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.js
@@ -0,0 +1,20 @@
+//// [tests/cases/conformance/importDefer/dynamicImportDeferInvalidStandalone.ts] ////
+
+//// [a.ts]
+export function foo() {
+ console.log("foo from a");
+}
+
+//// [b.ts]
+import.defer;
+
+(import.defer)("a");
+
+
+//// [a.js]
+export function foo() {
+ console.log("foo from a");
+}
+//// [b.js]
+import.defer;
+(import.defer)("a");
diff --git a/tests/baselines/reference/dynamicImportDeferInvalidStandalone.symbols b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.symbols
new file mode 100644
index 0000000000000..fa3e75096a4db
--- /dev/null
+++ b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.symbols
@@ -0,0 +1,18 @@
+//// [tests/cases/conformance/importDefer/dynamicImportDeferInvalidStandalone.ts] ////
+
+=== a.ts ===
+export function foo() {
+>foo : Symbol(foo, Decl(a.ts, 0, 0))
+
+ console.log("foo from a");
+>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
+>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
+>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
+}
+
+=== b.ts ===
+
+import.defer;
+
+(import.defer)("a");
+
diff --git a/tests/baselines/reference/dynamicImportDeferInvalidStandalone.types b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.types
new file mode 100644
index 0000000000000..c7c0f20db20de
--- /dev/null
+++ b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.types
@@ -0,0 +1,39 @@
+//// [tests/cases/conformance/importDefer/dynamicImportDeferInvalidStandalone.ts] ////
+
+=== a.ts ===
+export function foo() {
+>foo : () => void
+> : ^^^^^^^^^^
+
+ console.log("foo from a");
+>console.log("foo from a") : void
+> : ^^^^
+>console.log : (...data: any[]) => void
+> : ^^^^ ^^ ^^^^^
+>console : Console
+> : ^^^^^^^
+>log : (...data: any[]) => void
+> : ^^^^ ^^ ^^^^^
+>"foo from a" : "foo from a"
+> : ^^^^^^^^^^^^
+}
+
+=== b.ts ===
+import.defer;
+>import.defer : any
+> : ^^^
+>defer : any
+> : ^^^
+
+(import.defer)("a");
+>(import.defer)("a") : any
+> : ^^^
+>(import.defer) : any
+> : ^^^
+>import.defer : any
+> : ^^^
+>defer : any
+> : ^^^
+>"a" : "a"
+> : ^^^
+
diff --git a/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.errors.txt b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.errors.txt
new file mode 100644
index 0000000000000..dc89860117c4b
--- /dev/null
+++ b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.errors.txt
@@ -0,0 +1,13 @@
+b.ts(1,1): error TS18060: Deferred imports are only supported when the '--module' flag is set to 'esnext'.
+b.ts(1,14): error TS2792: Cannot find module 'a'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option?
+
+
+==== b.ts (2 errors) ====
+ import.defer("a").then(ns => {
+ ~~~~~~~~~~~~~~~~~
+!!! error TS18060: Deferred imports are only supported when the '--module' flag is set to 'esnext'.
+ ~~~
+!!! error TS2792: Cannot find module 'a'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option?
+ ns.foo();
+ });
+
\ No newline at end of file
diff --git a/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.js b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.js
new file mode 100644
index 0000000000000..12fdcdfee2978
--- /dev/null
+++ b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.js
@@ -0,0 +1,12 @@
+//// [tests/cases/conformance/importDefer/dynamicImportDeferMissingModuleESNext.ts] ////
+
+//// [b.ts]
+import.defer("a").then(ns => {
+ ns.foo();
+});
+
+
+//// [b.js]
+import.defer("a").then(ns => {
+ ns.foo();
+});
diff --git a/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.symbols b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.symbols
new file mode 100644
index 0000000000000..af6712cc15aae
--- /dev/null
+++ b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.symbols
@@ -0,0 +1,13 @@
+//// [tests/cases/conformance/importDefer/dynamicImportDeferMissingModuleESNext.ts] ////
+
+=== b.ts ===
+import.defer("a").then(ns => {
+>import.defer("a").then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --))
+>then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --))
+>ns : Symbol(ns, Decl(b.ts, 0, 23))
+
+ ns.foo();
+>ns : Symbol(ns, Decl(b.ts, 0, 23))
+
+});
+
diff --git a/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.types b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.types
new file mode 100644
index 0000000000000..d5af4f5334730
--- /dev/null
+++ b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.types
@@ -0,0 +1,35 @@
+//// [tests/cases/conformance/importDefer/dynamicImportDeferMissingModuleESNext.ts] ////
+
+=== b.ts ===
+import.defer("a").then(ns => {
+>import.defer("a").then(ns => { ns.foo();}) : Promise
+> : ^^^^^^^^^^^^^
+>import.defer("a").then : (onfulfilled?: (value: any) => TResult1 | PromiseLike, onrejected?: (reason: any) => TResult2 | PromiseLike) => Promise
+> : ^ ^^^^^^^^ ^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+>import.defer("a") : Promise
+> : ^^^^^^^^^^^^
+>import.defer : any
+> : ^^^
+>defer : any
+> : ^^^
+>"a" : "a"
+> : ^^^
+>then : (onfulfilled?: (value: any) => TResult1 | PromiseLike, onrejected?: (reason: any) => TResult2 | PromiseLike) => Promise
+> : ^ ^^^^^^^^ ^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+>ns => { ns.foo();} : (ns: any) => void
+> : ^ ^^^^^^^^^^^^^^
+>ns : any
+> : ^^^
+
+ ns.foo();
+>ns.foo() : any
+> : ^^^
+>ns.foo : any
+> : ^^^
+>ns : any
+> : ^^^
+>foo : any
+> : ^^^
+
+});
+
diff --git a/tests/cases/conformance/importDefer/dynamicImportDefer.ts b/tests/cases/conformance/importDefer/dynamicImportDefer.ts
new file mode 100644
index 0000000000000..103ff29df7c9d
--- /dev/null
+++ b/tests/cases/conformance/importDefer/dynamicImportDefer.ts
@@ -0,0 +1,10 @@
+// @module: esnext
+// @filename: a.ts
+export function foo() {
+ console.log("foo from a");
+}
+
+// @filename: b.ts
+import.defer("a").then(ns => {
+ ns.foo();
+});
diff --git a/tests/cases/conformance/importDefer/dynamicImportDeferInvalidStandalone.ts b/tests/cases/conformance/importDefer/dynamicImportDeferInvalidStandalone.ts
new file mode 100644
index 0000000000000..7e9db31ea2da2
--- /dev/null
+++ b/tests/cases/conformance/importDefer/dynamicImportDeferInvalidStandalone.ts
@@ -0,0 +1,10 @@
+// @module: esnext
+// @filename: a.ts
+export function foo() {
+ console.log("foo from a");
+}
+
+// @filename: b.ts
+import.defer;
+
+(import.defer)("a");
diff --git a/tests/cases/conformance/importDefer/dynamicImportDeferMissingModuleESNext.ts b/tests/cases/conformance/importDefer/dynamicImportDeferMissingModuleESNext.ts
new file mode 100644
index 0000000000000..7dd278d075558
--- /dev/null
+++ b/tests/cases/conformance/importDefer/dynamicImportDeferMissingModuleESNext.ts
@@ -0,0 +1,5 @@
+// @target: es2020
+// @filename: b.ts
+import.defer("a").then(ns => {
+ ns.foo();
+});