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.
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:
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:
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:
// 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:
// 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
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.
Frequently Asked Questions
Why does TypeScript require .js extensions instead of .ts?
What is the difference between a type-only import error and a missing module error?
Can I use .ts extensions in imports with allowImportingTsExtensions?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.