Recipe · Modules and packages

How do I type a dynamic import() in JavaScript?

  • Node.js (ES modules)
  • import()
  • typeof import
  • Lazy loading
  • Checked with TypeScript 5.9

The problem I load a charting module only when a report needs it, with await import(). Is the result typed, and how do I type a variable that holds the module once it is loaded?

The short answer

Await the import() and use the result; tsc types it from the module it loads. For a variable that holds the module after the first load, write its type as typeof import("./charts.js"):

report.js report.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/** @type {typeof import("./charts.js") | undefined} */
let charts;

/** @param {number[]} sales */
export async function salesLine(sales) {
  charts ??= await import("./charts.js");
  return charts.sparkline(sales);
}

const { sparkline } = await import("./charts.js");
console.log(sparkline([3, 9, 4]), await salesLine([1, 2, 3]));
let charts: typeof import("./charts.js") | undefined;

export async function salesLine(sales: number[]) {
  charts ??= await import("./charts.js");
  return charts.sparkline(sales);
}

const { sparkline } = await import("./charts.js");
console.log(sparkline([3, 9, 4]), await salesLine([1, 2, 3]));
A dynamic import() is typed from the module it loads.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js"]
}

sparkline from the destructured import has the module’s own signature, and charts.sparkline(sales) is checked through the cached module variable.

Why it works

A dynamic import("./charts.js") with a literal path resolves like a static import, so tsc knows which file it loads and types the result as Promise<typeof import("./charts.js")>, the module’s namespace object. typeof import("./charts.js") is the same type written in JSDoc: the type of the whole module, with every export as a property. Nothing about it loads the module early; it is only a type.

The same thing in TypeScript

The TypeScript equivalent tab annotates the variable with typeof import("./charts.js") | undefined, the same type expression the JSDoc uses. The import() calls themselves are plain JavaScript and stay the same.

What goes wrong

A misspelled export is TS2339 on the module object, and under nodenext a relative path without its file extension is TS2835, exactly as for a static import. A path built at runtime is not checked at all: loose is any, so loose.anything passes:

report.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: report.js(2,9): error TS2339: Property 'sparkLine' does not exist on type 'typeof import("/project/charts", { with: { "resolution-mode": "import" } })'. report.js(5,29): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './charts.js'? 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. report.js(2,9): error TS2339: Property 'sparkLine' does not exist on type 'typeof import("/project/charts", { with: { "resolution-mode": "import" } })'. report.js(5,29): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './charts.js'? 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. report.js(2,9): error TS2339: Property 'sparkLine' does not exist on type 'typeof import("/project/charts")'. report.js(5,29): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './charts.js'? checked 21 October 2026
const { sparkLine } = await import("./charts.js");error TS2339: Property 'sparkLine' does not exist on type 'typeof import("/project/charts", { with: { "resolution-mode": "import" } })'.

const charts = await import("./charts");error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './charts.js'?

const name = "charts";
const loose = await import(`./${name}.js`);
console.log(sparkLine, charts, loose.anything);
A misspelled export, a path without its extension, and a path tsc cannot see.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js"]
}

Gotchas

  • The error shows a full path. tsc prints the module’s type as typeof import("<path>/charts", …); read it as “the module charts.js”.
  • Default exports arrive as default. const { default: formatPrice } = await import("./price.js") gets a module’s default export.
  • Bundlers may split the code; tsc does not care. Code splitting happens in a bundler, and the types are the same with or without it.

Questions

What is the type of import() with a path built at runtime?

Promise<any>. tsc can only type a dynamic import whose specifier is a string literal; for a path built at runtime, cast the result to the module type you expect, or keep a map of literal imports.

Does import() work in CommonJS files?

Yes. Node.js supports import() in CommonJS modules, and tsc types it the same way. It is the usual way for CommonJS code to load an ES module.

Sources

Last reviewed 21 October 2026.