CommonJS and ES modules
tsx runs TypeScript in both of Node.js's module systems: CommonJS, which uses require() and module.exports, and ECMAScript modules (ESM), which use import and export. The execution format matters for top-level await, available globals, and the shape of imported values; Node.js's module documentation covers the underlying module systems.
Choosing a module format
Use an explicit extension or package.json setting so your project's format is clear:
| Intent | TypeScript | JavaScript |
|---|---|---|
| Always ESM | .mts | .mjs |
| Always CommonJS | .cts | .cjs |
| Follow the package's format | .ts, .tsx | .js |
For .ts and .js files, set "type": "module" or "type": "commonjs" in the nearest parent package.json. This fragment makes a package explicitly ESM:
package.json (field to merge into the existing file)
{
"type": "module"
}Keep your package's other fields. Changing type affects other files in that package, not just the script you are running. Use .mts or .cts if you only need to make one file's format explicit.
When a package has no explicit type, classification can depend on the Node.js version and loading path. tsx preserves Node's TypeScript module classification when Node provides one; its compatibility path otherwise uses the package type with a CommonJS default. Do not rely on an omitted type to select ESM consistently across runtimes.
The module setting in tsconfig.json configures TypeScript's compiler. It does not replace the runtime's extension and package-format rules.
Import syntax does not always mean ESM execution
You can write import and export in a TypeScript file that tsx transforms to CommonJS. The syntax alone does not guarantee native ESM execution.
- In ESM, top-level
awaitis available. Useimport.meta.urlto identify the current module. - In CommonJS, use
require(),__filename, and__dirname. Put asynchronous work inside an async function rather than using top-levelawait.
If you need top-level await, make the file or package explicitly ESM. The extension and package rules above select that execution format; an asynchronous loading API alone does not change a CommonJS file into ESM.
For example, with tsx installed, create this file in your project directory:
script.mts
const message: string = await Promise.resolve('Ready')
console.log(message)Run it from that directory:
npx tsx ./script.mtsIt prints:
ReadyImporting CommonJS from ESM
For a native ESM default import, Node.js provides the CommonJS module's complete module.exports value. A package that exports a function directly can be imported as that function. A package that exports an object with a default property is imported as that object.
This differs from some compiler-generated CommonJS interop helpers. Changing esModuleInterop does not change Node's native ESM default-import behavior. Use a documented named export if the package provides one; otherwise check the package's entry points and report an export mismatch to its maintainer.
Loading ESM from CommonJS
tsx supports loading many ESM dependencies from CommonJS. Synchronous require() cannot execute a module graph that requires top-level await; use an asynchronous import path for that case.
If you are building a tool that loads TypeScript files, choose between tsImport() for asynchronous loading and tsx.require() for synchronous CommonJS execution. Their scope and caching behavior differ from registering tsx for an entire process.
For Node.js's underlying rules, see package module types and ESM interoperability.
