Skip to content

Instantly share code, notes, and snippets.

@hsnice16
Created April 5, 2025 18:49
Show Gist options
  • Save hsnice16/ba6082ce24bf91514361140e0726e12e to your computer and use it in GitHub Desktop.
Save hsnice16/ba6082ce24bf91514361140e0726e12e to your computer and use it in GitHub Desktop.
Why do we explicit mention `*.js` in import and export statements?

Q. I was checking the TypeScript SDK code, and stumbled upon one thing. I noticed that even though they are using TypeScript, in the index.ts file and all other files they are using *.js to import and export. Why is that required for a npm library?

A. This is an interesting observation! In a TypeScript project, you might expect to see .ts files being imported and exported directly, but seeing .js extensions in a TypeScript SDK like this is actually a deliberate choice tied to how the library is consumed after being built and published to npm. Let me explain why this happens and why it’s common for an npm library.

Why .js instead of .ts in imports/exports?

  1. Output Files Are JavaScript:
    When you write a TypeScript library, the .ts files are compiled to JavaScript (.js) as part of the build process (e.g., using tsc or a bundler like Rollup/Webpack). The compiled .js files are what get published to npm, not the original .ts source files. Since the library’s consumers will be working with the compiled JavaScript, the import/export paths in the source code are often written to reflect the final output file extensions (.js) rather than the source (.ts).

  2. Module Resolution in Node.js:
    Node.js (and most JavaScript runtimes) resolves modules without requiring file extensions in require or import statements—it automatically looks for .js (or other supported extensions) files. However, when using explicit extensions in TypeScript, .js aligns with what will exist in the dist folder after compilation. TypeScript itself understands that a .js import in a .ts file refers to the corresponding .ts file during development, and it handles the mapping internally.

  3. TypeScript’s allowJs and Emit Behavior:
    TypeScript doesn’t require you to change .js imports to .ts in your source code because it’s designed to work seamlessly with JavaScript ecosystems. When you import something.js in a .ts file, TypeScript resolves it to something.ts during compilation (assuming the file exists), and the output in the dist folder will be something.js. This makes the codebase consistent between source and output, avoiding the need to rewrite imports.

  4. Compatibility with ESM and CommonJS:
    Modern npm libraries often target ECMAScript Modules (ESM) and/or CommonJS as their output formats. Both use .js as the standard file extension. By using .js in the imports/exports, the library ensures compatibility with these module systems once published, regardless of whether the source was written in TypeScript.

  5. Published Package Consistency:
    When users install your npm package, they don’t typically get the .ts files (unless you’re publishing source code or type definitions explicitly). They get the compiled .js files along with .d.ts declaration files for TypeScript users. If the library’s internal imports used .ts extensions, it would mismatch the actual files in the published package, causing resolution errors.

Example

Imagine this in index.ts:

export * from './utils.js';

Here, utils.ts exists in the source code, but after compilation, it becomes utils.js in the dist folder. TypeScript understands this during development, and the package.json (e.g., "main": "dist/index.js") points consumers to the compiled output. Using .js in the import ensures the paths remain valid post-compilation.

Why Not Use Extensionless Imports?

You might wonder why they didn’t just omit the extensions (e.g., export * from './utils'). While this works in many cases due to Node’s module resolution, explicitly adding .js is a growing convention, especially with ESM and tools like TypeScript, Deno, and bundlers. It avoids ambiguity and ensures the imports are explicit about targeting JavaScript output, which is particularly helpful when dealing with strict module resolution settings (e.g., resolve.extensions in Webpack or "moduleResolution": "node16" in tsconfig.json).

Is It Required?

It’s not strictly required for an npm library to use .js extensions in TypeScript source files—TypeScript is flexible enough to handle extensionless imports or even .ts imports if configured properly. However, using .js is a practical choice to:

  • Align with the compiled output.
  • Ensure compatibility with JavaScript ecosystems.
  • Avoid additional build-time transformations to rewrite imports.

Conclusion

The use of .js in the TypeScript SDK’s index.ts and other files is a design decision to keep the source code consistent with the compiled output that npm users will consume. It’s a common practice in TypeScript-based npm libraries to bridge the gap between TypeScript development and JavaScript deployment. If you’re curious about the specific SDK you’re looking at, check its tsconfig.json (module and outDir settings) and package.json (main or module fields) to see how it’s configured!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment