Module Import Mistakes in TypeScript

Common module import mistakes in TypeScript can cause compile errors, runtime crashes, and confusing type behavior. Learn what to avoid and how to fix each mistake.

7 min read

TypeScript import mistakes are among the most common errors in TypeScript projects. They range from compile-time errors that TypeScript catches immediately to runtime crashes that only surface in production. Understanding the most frequent mistakes helps you avoid them and fix them quickly.

Missing .js Extensions with nodenext

The most common import mistake when using moduleResolution nodenext is writing extensionless relative imports. Node.js ESM requires full file extensions, and TypeScript enforces this.

The wrong way omits the extension entirely:

typescripttypescript
import { add } from "./math";

The correct way includes the .js extension that Node.js expects at runtime, even though the file on disk is named math.ts:

typescripttypescript
import { add } from "./math.js";

The error message is clear but can be confusing at first. TypeScript reports that it cannot find the module, and the fix is to add the .js extension.

TypeScript resolves .js to your .ts source file during compilation, so the extension is correct even though the file on disk is .ts.

Importing a Type as a Value

Interfaces and type aliases do not exist at runtime. Importing them with a regular import and then trying to use them as a value is a mistake TypeScript catches before your code ever runs.

The source module exports an interface. It has no runtime representation:

typescripttypescript
// models.ts
export interface User {
  id: string;
  name: string;
}

The consumer imports it like a value and tries to construct it. An interface has no constructor to call, so this never reaches the runtime:

typescripttypescript
// app.ts
import { User } from "./models.js";
 
const u = new User();

TypeScript rejects this at compile time with error TS2693: 'User' only refers to a type, but is being used as a value here. The code never compiles, so the interface being erased never becomes a runtime problem.

The fix is to use import type when you only need the type. If you need a runtime value, make sure the export is a class or function. For more, see type-only imports in TypeScript.

Writing .ts Extensions in Imports

Writing .ts extensions in import paths is a natural instinct that TypeScript prevents for good reason. The compiler rejects imports ending in .ts because the compiled JavaScript would contain a path that no runtime can resolve.

Even with allowImportingTsExtensions enabled, this only works with noEmit or emitDeclarationOnly. The option exists for projects where a bundler processes TypeScript files directly. Always write .js extensions and let TypeScript perform extension substitution.

Barrel File Over-Importing

Barrel files that re-export many modules can cause every module to load even when the consumer only needs one export. This slows down editor startup and can increase bundle size.

When a consumer imports only one function from a barrel, the editor still loads all the re-exported modules to provide accurate autocomplete for the barrel's entire API surface. In large projects, this can add significant latency.

The fix is to use direct imports for performance-sensitive code and keep barrels at module boundaries rather than deep inside implementation folders. For more, see barrel files in TypeScript.

Path Alias Mismatches

Path aliases configured in tsconfig paths must also be configured in the bundler or runtime. Forgetting the second step causes runtime crashes even though TypeScript is perfectly happy with the imports.

TypeScript resolves the alias and checks types successfully. But the compiled JavaScript still contains the alias prefix, since paths only affects how TypeScript resolves imports during type checking, not what it emits.

If the bundler is not configured with the same alias, the code crashes with a module not found error. For plain tsc builds without a bundler, a tool like tsc-alias can rewrite alias prefixes in the compiled output after compilation finishes.

The fix is to mirror every path alias in your build tool configuration. Every alias must exist in two places: tsconfig.json and your build configuration. For more, see path aliases in TypeScript.

Using export * Unintentionally

The wildcard re-export syntax exports every named export from a module, including internal helpers that were never meant to be public. If those helpers are later renamed or removed, consumers that depended on them will break.

The fix is to use explicit re-exports instead of the wildcard. List only the names you intend to make public. This gives you full control over your module's API surface.

Mixing Default and Named Import Syntax

Forgetting the curly braces on named imports or adding them on default imports causes confusing errors. A default import uses no curly braces, while a named import always does.

If a module uses both default and named exports, make sure the syntax matches each one. TypeScript reports exactly which style is wrong.

Importing from the Wrong Module Path

TypeScript resolves relative paths, bare specifiers, and path aliases differently. A relative path like ./math.js resolves to a file next to the importing file, while a bare specifier like react triggers a node_modules package lookup.

Using the wrong style for the target module is a frequent source of confusion.

Circular Imports

When module A imports from module B and module B imports from module A, the dependency graph has a cycle. Depending on how the modules access each other's exports, this can throw a runtime error or silently produce an incomplete value.

Break the cycle by extracting shared code into a third module. For a detailed guide, see avoid circular imports in TypeScript.

Rune AI

Rune AI

Key Insights

  • Always include .js extensions in relative imports when using nodenext moduleResolution.
  • Use import type for type-only imports to prevent accidental runtime references to erased types.
  • Avoid barrel files that re-export from other barrel files for performance.
  • Configure both TypeScript and your bundler with the same path aliases.
  • Never write .ts extensions in imports. TypeScript prevents this because the compiled output would be invalid.
RunePowered by Rune AI

Frequently Asked Questions

Why does TypeScript require .js extensions instead of .ts?

TypeScript models the runtime. Browsers and Node.js resolve .js files, not .ts files. TypeScript uses extension substitution to resolve the .js extension to your .ts source during compilation.

What is the difference between a type-only import error and a missing module error?

A type-only import error means you are trying to use a type as a runtime value. A missing module error means TypeScript cannot find the file at the specified path. The first is a type system issue. The second is a file resolution issue.

Can I use .ts extensions in imports with allowImportingTsExtensions?

Yes, but only with noEmit or emitDeclarationOnly. The compiled JavaScript would contain .ts extensions that no runtime can resolve. This option is designed for projects where a bundler processes TypeScript files directly.

Conclusion

Import mistakes are some of the most common and frustrating TypeScript errors. Missing .js extensions, importing types as values, barrel file over-importing, and path alias mismatches each have straightforward fixes once you understand why they happen. TypeScript models the runtime. Every import rule exists to prevent a runtime crash.