| |

TypeScript 55 ๐Ÿ”ท Modules โ€” import and export

TypeScript’s module system is where a lot of confusion lives, because there are two overlapping things: the JavaScript module syntax that ships in the runtime, and the TypeScript type-only import/export syntax that is erased at compile time. On top of that, TypeScript offers several module resolution modes (node, node16, nodenext, bundler, classic) that change how import "foo" is resolved, and several compiler options (module, moduleResolution, esModuleInterop, verbatimModuleSyntax, isolatedModules) that decide what the emitted JavaScript looks like. A chapter that ignores these layers produces code that works in one project and breaks in another. This chapter covers both the surface syntax and the configuration that decides how it behaves, with an emphasis on the rules that avoid the common failure modes.

Key point: import and export come in three forms: value imports (used at runtime), type-only imports (import type or import { type Foo }, erased at compile time), and side-effect imports (import "./polyfill", run for effect, no binding). Modern TypeScript recommends verbatimModuleSyntax: true so that what you write is what is emitted, and requires type-only imports to be marked. Module resolution is governed by moduleResolution (node16, nodenext, bundler, or node), and the choice affects whether extensions are required in import paths, whether exports in package.json is honored, and whether .js must appear in TypeScript source importing a .ts file (it must, under node16/nodenext).


Why module systems matter in TypeScript

JavaScript had no module system in the language for its first two decades. In the browser, code was loaded with <script> tags in order. On the server, Node invented CommonJS โ€” require() and module.exports. The language eventually standardized ES Modules โ€” import and export โ€” and the transition is still in progress.

Two runtimes, two systems. Node shipped CommonJS first and added ESM support only in version 12, stabilized in later versions. Browsers support only ESM. Bundlers (Webpack, Vite, esbuild) handle both. This is why package.json has a "type": "module" field (opt in to ESM) and why .mjs and .cjs extensions exist as overrides.

TypeScript sits on top. TypeScript compiles to one or the other depending on the module compiler option. It can emit CommonJS, ESM, or both, and the emitted code has to match what the target runtime expects. Getting this wrong produces require is not defined in ESM, or Cannot use import statement outside a module in CommonJS.

Why types are separate from values. TypeScript types are erased at runtime. An import of a type is not an import of a value โ€” the import statement only exists for the compiler, and if it is emitted, the runtime will fail because there is no such export. This is why import type exists: it tells the compiler that the import is type-only and should not be emitted.

Why the configuration is decisive. The same import { User } from "./user" line behaves differently depending on whether User is a type or a value, whether verbatimModuleSyntax is on, and whether the resolver is node16 or bundler. A chapter that shows only the syntax hides the configuration that decides whether it works.

Why this chapter is dense. Modules touch every file in a project. The syntax is small, but the configuration space is large, and a wrong combination produces errors that are hard to diagnose. The goal here is to present the syntax, then the configuration, then the rules that tie them together, so that a reader can choose a configuration and know why it is right.


Value imports and exports

A value import brings a runtime binding into the file. The binding is available in expressions at runtime.

// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

export const PI = 3.14159;

// app.ts
import { add, PI } from "./math";

console.log(add(1, 2));
console.log(PI);

The export keyword marks the binding as available to other modules. The import { ... } brings the named bindings in. Both the function and the constant are values, and both are emitted at runtime.

Default exports. A module can export one default binding, which is imported without braces.

// logger.ts
export default class Logger {
  log(message: string): void {
    console.log(message);
  }
}

// app.ts
import Logger from "./logger";
const logger = new Logger();

A default export is a single value, and the importing file can name it whatever it wants. There is exactly one per module.

Namespace imports. An entire module can be imported as a namespace object.

import * as math from "./math";

console.log(math.add(1, 2));
console.log(math.PI);

The namespace object contains all the named exports of the module. It is useful when the module has many exports and a short prefix is preferable to listing them all.

Re-exports. A module can re-export from another module, which is how a public API is assembled from internal modules.

// index.ts
export { add, PI } from "./math";
export { default as Logger } from "./logger";
export * from "./types";

The export ... from syntax re-exports without importing into the file’s local scope. The export * re-exports everything that is exported by the target module. This is the standard pattern for a barrel file that collects a package’s public API.

Why named exports are preferred over default. Named exports are greppable โ€” searching for add finds the export and every import. Default exports are not โ€” the importing file can rename the default arbitrarily, and the connection is not visible in a search. Named exports also compose better with re-exports. Default is allowed and common, but named is the recommended default for new code.


Type-only imports and exports

A type import brings a type into the file, and the import is erased at compile time. The runtime never sees it.

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

export type Role = "admin" | "user";

// app.ts
import type { User, Role } from "./user";

function describe(user: User): Role {
  return user.id === "1" ? "admin" : "user";
}

The import type marks the import as type-only. The compiler uses it for checking, and the emitted JavaScript has no import for ./user.

Inline type modifiers. A mixed import can mark individual bindings as type-only.

import { add, type User } from "./math-and-types";

The add is a value import, and User is a type import. This is useful when a module exports both values and types and the importing file needs both.

Type-only exports. The export type form marks an export as type-only.

// user.ts
export type { User };

This is used when re-exporting types from a barrel. It signals that the export is erased at compile time and cannot be imported as a value.

Why type-only imports matter. Without them, an import of a type that is only used in a type position is still emitted by some configurations, and the runtime fails because there is no such export. With them, the import is erased and the runtime sees nothing. The verbatimModuleSyntax option enforces the marking, which is why it is recommended.

Why the distinction is enforced by some configs. Under verbatimModuleSyntax, TypeScript emits imports exactly as written. If a type is imported without type, and the module is emitted, the import is kept and the runtime fails. The option forces the developer to mark type-only imports correctly, which prevents the failure.


Side-effect imports

A side-effect import runs a module for its effect and does not bind anything.

import "./polyfills";
import "reflect-metadata";

The module is loaded and executed, and any global state it sets up is available. Nothing is imported into the file’s scope. This pattern is used for polyfills, for libraries that install themselves on a global, and for modules that register themselves with a framework.

Why side-effect imports are erased by some bundlers. A bundler that tree-shakes removes imports it believes have no effect. A side-effect import with no binding can be removed if the bundler thinks the module is pure. The "sideEffects" field in package.json marks modules that must not be removed. Libraries that rely on side effects declare them, and the bundler respects the declaration.

Why side-effect imports should be rare. A module that runs code on import is harder to reason about than a module that exports functions. The effect is invisible at the import site. Side-effect imports are a tool, not a default, and they are best confined to the entry points of the application.


verbatimModuleSyntax and isolatedModules

Two compiler options determine how imports and exports are emitted. Understanding them prevents a class of runtime failures.

isolatedModules. A file that is transpiled in isolation โ€” by a bundler like esbuild or Babel that does not see the whole program โ€” cannot know whether an import is a type or a value. TypeScript’s isolatedModules option enforces rules that make each file independently transpilable. Under it, export { SomeType } is an error because the transpiler cannot tell if SomeType is a type or a value; the fix is export type { SomeType }.

verbatimModuleSyntax. This option makes the emitted imports match the written imports exactly. No import is elided, no import is added. The developer is responsible for marking type-only imports with import type or the type modifier, and the compiler will not remove them for you. The benefit is predictability: what you write is what runs.

// With verbatimModuleSyntax, this is an error if User is a type:
import { User } from "./user";

// The fix:
import type { User } from "./user";

Why these options are recommended. Modern tooling โ€” Vite, esbuild, SWC, Babel โ€” transpiles files in isolation. TypeScript’s traditional behavior of eliding type-only imports at compile time does not work in that mode, because the transpiler is not TypeScript. The verbatimModuleSyntax and isolatedModules options align TypeScript’s behavior with what the rest of the toolchain expects.

Why the combination is the modern default. A new project in 2026 with Vite or a similar bundler should enable both. The cost is the discipline of marking type imports. The benefit is that the emitted code is what the developer wrote, and the runtime behavior is predictable.


Module resolution and extensions

The moduleResolution option determines how the compiler finds the module for a given import path. The modes differ in what they accept and what they require.

node (or node10). The legacy Node.js resolution. It accepts extensionless imports and resolves through node_modules, index.js, and package.json main. It is the traditional behavior and remains common for libraries that target bundlers.

node16 and nodenext. The modern Node.js resolution. It honors the exports field in package.json, requires file extensions in relative imports (.js, .mjs, .cjs), and distinguishes ESM from CommonJS by the file extension and the "type" field. Under these modes, a TypeScript source file that imports ./user.ts must write import { User } from "./user.js" โ€” the .js extension, because that is what the emitted file will be. This is the rule that surprises developers the most.

bundler. A resolution mode designed for code that will be processed by a bundler. It accepts extensionless imports and honors the exports field. It is the recommended mode for applications built with Vite, Webpack, or esbuild.

classic. The original TypeScript resolution, rarely used today. It resolves relative to the file and does not look in node_modules the way Node does.

// tsconfig.json โ€” application with a bundler
{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "bundler",
    "verbatimModuleSyntax": true,
    "isolatedModules": true
  }
}

// tsconfig.json โ€” Node.js library
{
  "compilerOptions": {
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "verbatimModuleSyntax": true
  }
}

Why the extension rule exists. Node.js requires the file extension in a relative import for ESM. TypeScript’s emitted file is .js, so the import path in the source must include .js to match what the runtime will resolve. The .ts extension is never written in an import โ€” it would be an error in the emitted file.

Why the choice must match the runtime. A project that targets Node must use nodenext. A project that targets a browser bundle must use bundler. Using the wrong one produces resolution errors or, worse, code that compiles and fails at runtime.


esModuleInterop and default imports

CommonJS modules export a single value via module.exports. ESM modules have named exports and a default. When an ESM file imports a CommonJS module, the shapes do not match. The esModuleInterop option adds the compatibility shim.

Without esModuleInterop. Importing a CommonJS module like express requires import * as express from "express" and the resulting namespace is awkward.

With esModuleInterop. import express from "express" works, and the default import maps to the CommonJS module’s export. This is what most code expects, and the option is enabled by strict or recommended separately.

allowSyntheticDefaultImports. This option allows the default import syntax without the runtime shim. It is useful for type-checking when the runtime compatibility is provided by a bundler. It is implied by esModuleInterop.

Why the option is almost always on. Modern projects enable it because almost every import from the npm ecosystem expects the default-import form. Disabling it produces code that is valid but awkward, and the awkwardness spreads to every file that imports a CommonJS module.


Module boundaries and circular imports

A circular import is when a.ts imports from b.ts and b.ts imports from a.ts. JavaScript and TypeScript allow it, but the behavior is subtle, because the order in which the modules are initialized matters.

How circular imports happen. Two modules with a mutual dependency โ€” a service and a factory that references it, a base class and a subclass, a type and a function that uses it. The cycle is often not obvious until the runtime fails.

Why the failure is confusing. One module runs first and sees the other module’s exports as partially initialized. A binding that is not yet assigned may be undefined at the moment it is used. The error is at runtime and depends on the order of module evaluation, which is determined by the bundler or the runtime.

How to break a cycle. The general strategies are: extract the shared dependency into a third module that both import, use a type-only import for one direction (types are erased, so no runtime cycle), or defer the use of one of the modules until the function is called.

// a.ts
import type { B } from "./b";  // type-only, no runtime cycle
export function useB(b: B): void { /* ... */ }

The type-only import breaks the runtime cycle because it is erased. The type is still checked, but the runtime sees no import from b.ts. This is a common and effective technique.

Why the cycle should be found early. A cycle that is benign at one point can become a problem when the initialization order changes or the code is refactored. Tools like madge and dpdm detect cycles statically and report them. Running the tool in CI keeps the cycles out before they cause runtime failures.


Complete Example Session

// ============================================
// PART 1: VALUE EXPORT AND IMPORT
// ============================================

// math.ts
export function add(a: number, b: number): number {
  return a + b;
}
export const PI = 3.14159;

// app.ts
import { add, PI } from "./math";
console.log(add(1, 2), PI);

// ============================================
// PART 2: DEFAULT EXPORT
// ============================================

// logger.ts
export default class Logger {
  log(message: string): void {
    console.log(message);
  }
}

// app.ts
import Logger from "./logger";
new Logger().log("hello");

// ============================================
// PART 3: NAMESPACE IMPORT
// ============================================

import * as math from "./math";
console.log(math.add(1, 2), math.PI);

// ============================================
// PART 4: RE-EXPORTS
// ============================================

// index.ts
export { add, PI } from "./math";
export { default as Logger } from "./logger";
export * from "./types";

// ============================================
// PART 5: TYPE-ONLY IMPORT
// ============================================

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

// app.ts
import type { User } from "./user";

function greet(user: User): string {
  return `Hello, ${user.name}`;
}

// ============================================
// PART 6: INLINE TYPE MODIFIER
// ============================================

// mixed.ts
export const answer = 42;
export interface Question {
  text: string;
}

// app.ts
import { answer, type Question } from "./mixed";

console.log(answer);
const q: Question = { text: "?" };

// ============================================
// PART 7: SIDE-EFFECT IMPORT
// ============================================

import "./polyfills";
import "reflect-metadata";

// ============================================
// PART 8: NODE16 EXTENSION RULE
// ============================================

// Under moduleResolution: nodenext

// user.ts
export interface User { id: string; }

// app.ts
import type { User } from "./user.js";  // note the .js extension
// import { User } from "./user";  // โŒ error: extension required

// ============================================
// PART 9: BREAKING A CIRCULAR IMPORT
// ============================================

// a.ts
// import { B } from "./b";          // runtime cycle
import type { B } from "./b";       // type-only, erased

export function handle(b: B): void {
  console.log(b.value);
}

// b.ts
import { handle } from "./a";
export interface B { value: string; }
export function run(): void {
  handle({ value: "x" });
}

// ============================================
// PART 10: TSCONFIG FOR AN APPLICATION
// ============================================

// {
//   "compilerOptions": {
//     "target": "es2022",
//     "module": "esnext",
//     "moduleResolution": "bundler",
//     "verbatimModuleSyntax": true,
//     "isolatedModules": true,
//     "esModuleInterop": true,
//     "strict": true,
//     "skipLibCheck": true
//   }
// }

The ten parts cover value imports, defaults, namespaces, re-exports, type-only imports, inline type modifiers, side-effect imports, the Node16 extension rule, circular imports, and a modern tsconfig.json.


Quick Reference

Import Forms

FormExample
Named valueimport { foo } from "./mod"
Default valueimport foo from "./mod"
Namespaceimport * as ns from "./mod"
Named typeimport type { Foo } from "./mod"
Inline typeimport { foo, type Foo } from "./mod"
Side effectimport "./mod"

Export Forms

FormExample
Named valueexport const x = 1
Named typeexport type Foo = ...
Defaultexport default class {}
Re-exportexport { x } from "./mod"
Re-export allexport * from "./mod"
Type re-exportexport type { Foo } from "./mod"

Compiler Options

OptionEffect
moduleOutput format (esnext, commonjs, nodenext)
moduleResolutionResolver (bundler, node16, nodenext, node)
verbatimModuleSyntaxEmit imports as written
isolatedModulesEnforce per-file transpilation rules
esModuleInteropCommonJS/ESM compatibility
allowSyntheticDefaultImportsAllow default imports of CJS

Resolution Modes

ModeExtensionsexportsFor
bundlerOptionalโœ…Bundled apps
node16 / nodenextRequiredโœ…Node.js
node / node10OptionalโŒLegacy
classicOptionalโŒLegacy TypeScript

Circular Imports

TechniqueEffect
Extract shared moduleBreaks the cycle
import typeType-only, erased
Defer use to function callRuntime cycle avoided
madge, dpdmDetect cycles

Best Practices

โœ… Do This:

// Use named exports for greppability
export function add(a: number, b: number): number { return a + b; } // โœ…

// Mark type-only imports
import type { User } from "./user";                            // โœ…

// Use inline type modifiers for mixed imports
import { add, type User } from "./mixed";                      // โœ…

// Enable verbatimModuleSyntax
// tsconfig: "verbatimModuleSyntax": true                      // โœ…

// Use moduleResolution: bundler for apps
// tsconfig: "moduleResolution": "bundler"                     // โœ…

// Use the .js extension under node16/nodenext
import { User } from "./user.js";                              // โœ…

// Re-export from a barrel for the public API
export { add } from "./math";                                  // โœ…

// Break cycles with type-only imports
import type { B } from "./b";                                  // โœ…

โŒ Don’t Do This:

// Don't import types without the type marker when verbatimModuleSyntax is on
import { User } from "./user";  // โŒ if User is a type              // โš ๏ธ

// Don't mix module systems carelessly
// CommonJS require in an ESM file fails at runtime                // โš ๏ธ

// Don't write .ts extensions in imports
import { User } from "./user.ts";  // โŒ                           // โš ๏ธ

// Don't rely on default exports for searchability
export default function doStuff() {}                             // โš ๏ธ

// Don't ignore circular imports
// They fail at runtime depending on order                         // โš ๏ธ

// Don't set moduleResolution to node for a new project
// It ignores the exports field                                   // โš ๏ธ

// Don't assume side-effect imports are kept
// Bundlers may remove them without "sideEffects"                  // โš ๏ธ

Common Pitfalls

PitfallProblemSolution
Type import not markedRuntime error under verbatimModuleSyntaxUse import type
Missing .js extensionError under node16Add .js to relative imports
.ts in import pathCompile errorUse .js
Circular importRuntime undefinedBreak with import type
Default export renamedNot greppableUse named exports
Wrong moduleResolutionResolution errorsMatch the runtime
esModuleInterop offAwkward CJS importsEnable it
Side-effect import removedEffect missingSet "sideEffects" in package.json

Real-World Examples

1. Named value import

import { add, PI } from "./math";

2. Default import

import Logger from "./logger";

3. Namespace import

import * as math from "./math";

4. Type-only import

import type { User } from "./user";

5. Inline type modifier

import { add, type User } from "./mixed";

6. Re-export

export { add } from "./math";

7. Barrel file

export * from "./math";
export * from "./logger";

8. Side-effect import

import "./polyfills";

9. Node16 extension

import { User } from "./user.js";

10. Breaking a cycle

import type { B } from "./b";

Visual: Import Forms

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  VALUE IMPORT                                            โ”‚
โ”‚    import { add } from "./math"                          โ”‚
โ”‚    Runtime binding, emitted.                             โ”‚
โ”‚                                                          โ”‚
โ”‚  TYPE-ONLY IMPORT                                        โ”‚
โ”‚    import type { User } from "./user"                    โ”‚
โ”‚    Compile-time only, erased.                            โ”‚
โ”‚                                                          โ”‚
โ”‚  INLINE TYPE MODIFIER                                    โ”‚
โ”‚    import { add, type User } from "./mixed"              โ”‚
โ”‚    Mixed: add is a value, User is a type.                โ”‚
โ”‚                                                          โ”‚
โ”‚  SIDE-EFFECT IMPORT                                      โ”‚
โ”‚    import "./polyfills"                                  โ”‚
โ”‚    Runs for effect, no binding.                          โ”‚
โ”‚                                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Module Resolution

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  bundler                                                 โ”‚
โ”‚    import { x } from "./mod"       โœ…                    โ”‚
โ”‚    import { x } from "./mod.js"    โœ…                    โ”‚
โ”‚    Honours "exports" in package.json                     โ”‚
โ”‚                                                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  node16 / nodenext                                       โ”‚
โ”‚    import { x } from "./mod"       โŒ extension required โ”‚
โ”‚    import { x } from "./mod.js"    โœ…                    โ”‚
โ”‚    Honours "exports" in package.json                     โ”‚
โ”‚                                                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  node (legacy)                                           โ”‚
โ”‚    import { x } from "./mod"       โœ…                    โ”‚
โ”‚    Ignores "exports"                                     โ”‚
โ”‚                                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: verbatimModuleSyntax

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  WITHOUT verbatimModuleSyntax                            โ”‚
โ”‚                                                          โ”‚
โ”‚  Source:                                                 โ”‚
โ”‚    import { User } from "./user"                         โ”‚
โ”‚                                                          โ”‚
โ”‚  Emitted:                                                โ”‚
โ”‚    (nothing โ€” TypeScript elides the type-only import)    โ”‚
โ”‚                                                          โ”‚
โ”‚  Works if the compiler does the eliding.                 โ”‚
โ”‚  Fails if a transpiler (esbuild, Babel) does it.         โ”‚
โ”‚                                                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  WITH verbatimModuleSyntax                               โ”‚
โ”‚                                                          โ”‚
โ”‚  Source:                                                 โ”‚
โ”‚    import { User } from "./user"                         โ”‚
โ”‚    โŒ error: User is a type, must use "import type"      โ”‚
โ”‚                                                          โ”‚
โ”‚  Source:                                                 โ”‚
โ”‚    import type { User } from "./user"                    โ”‚
โ”‚    โœ… emitted: (nothing)                                 โ”‚
โ”‚                                                          โ”‚
โ”‚  Source:                                                 โ”‚
โ”‚    import { add } from "./math"                          โ”‚
โ”‚    โœ… emitted: import { add } from "./math"              โ”‚
โ”‚                                                          โ”‚
โ”‚  What you write is what is emitted.                      โ”‚
โ”‚                                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Circular Import

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  a.ts imports b.ts                                       โ”‚
โ”‚  b.ts imports a.ts                                       โ”‚
โ”‚                                                          โ”‚
โ”‚  Runtime evaluation order:                               โ”‚
โ”‚    a.ts starts                                           โ”‚
โ”‚      โ””โ”€โ”€ imports b.ts                                    โ”‚
โ”‚            โ””โ”€โ”€ imports a.ts (partial)                    โ”‚
โ”‚                  โ””โ”€โ”€ a.ts's exports not yet assigned     โ”‚
โ”‚            โ””โ”€โ”€ b.ts uses a's export โ†’ undefined          โ”‚
โ”‚      โ””โ”€โ”€ a.ts finishes                                   โ”‚
โ”‚                                                          โ”‚
โ”‚  The failure depends on which module runs first.         โ”‚
โ”‚                                                          โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  BREAK WITH import type                                  โ”‚
โ”‚                                                          โ”‚
โ”‚  a.ts: import type { B } from "./b"                      โ”‚
โ”‚    Type-only, erased at compile time.                    โ”‚
โ”‚    No runtime import from b.ts.                          โ”‚
โ”‚    The cycle is broken.                                  โ”‚
โ”‚                                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: tsconfig for an App

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  {                                                       โ”‚
โ”‚    "compilerOptions": {                                  โ”‚
โ”‚      "target": "es2022",                                 โ”‚
โ”‚      "module": "esnext",                                 โ”‚
โ”‚      "moduleResolution": "bundler",                      โ”‚
โ”‚      "verbatimModuleSyntax": true,                       โ”‚
โ”‚      "isolatedModules": true,                            โ”‚
โ”‚      "esModuleInterop": true,                            โ”‚
โ”‚      "strict": true,                                     โ”‚
โ”‚      "skipLibCheck": true                                โ”‚
โ”‚    }                                                     โ”‚
โ”‚  }                                                       โ”‚
โ”‚                                                          โ”‚
โ”‚  module: esnext       โ†’ emit ESM                         โ”‚
โ”‚  moduleResolution: bundler โ†’ no extension required       โ”‚
โ”‚  verbatimModuleSyntax: true โ†’ mark type imports          โ”‚
โ”‚  isolatedModules: true โ†’ per-file transpilable           โ”‚
โ”‚  esModuleInterop: true โ†’ CJS compatibility               โ”‚
โ”‚                                                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Value importimport { foo } from "./mod"
Default importimport foo from "./mod"
Namespace importimport * as ns from "./mod"
Type-only importimport type { Foo } from "./mod"
Inline typeimport { foo, type Foo } from "./mod"
Side effectimport "./mod"
Re-exportexport { x } from "./mod"
Re-export allexport * from "./mod"
Verbose import"verbatimModuleSyntax": true
Per-file transpile"isolatedModules": true
CJS compat"esModuleInterop": true
Bundler resolve"moduleResolution": "bundler"
Node resolve"moduleResolution": "nodenext"

Key takeaways:

  • Imports and exports come in three forms โ€” value, type-only, and side-effect โ€” and the form determines whether the import exists at runtime
  • Type-only imports are erased at compile time โ€” import type and the inline type modifier mark them, and they never appear in the emitted JavaScript
  • verbatimModuleSyntax makes the emitted imports match the written imports โ€” the developer marks type imports, and the compiler does not elide them
  • isolatedModules aligns TypeScript with bundlers โ€” the option enforces rules that make each file independently transpilable, which is what esbuild, Vite, and Babel require
  • moduleResolution decides how import paths resolve โ€” bundler accepts extensionless imports, node16 and nodenext require .js extensions, and the choice must match the runtime
  • The .js extension is required under node16 and nodenext โ€” the source imports the future .js file, not the .ts file, because the emitted code is .js
  • Circular imports are allowed but dangerous โ€” the failure depends on module evaluation order, and the standard fix is to break the cycle with a type-only import or a third module
  • esModuleInterop enables the default-import form for CommonJS modules, which is what most code expects
  • Named exports are preferred over default for greppability and re-export composition, though default exports are allowed
  • The configuration and the syntax must agree โ€” a project with the wrong moduleResolution or a missing type marker compiles in one environment and fails in another

Remember: The module syntax is small, but the configuration space is large, and the two must agree. Mark type-only imports, choose a moduleResolution that matches the runtime, enable verbatimModuleSyntax and isolatedModules for modern toolchains, and break circular imports with type-only imports. The rules are the difference between code that compiles and runs everywhere and code that works only in the environment it was written for.


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!