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
| Form | Example |
|---|---|
| Named value | import { foo } from "./mod" |
| Default value | import foo from "./mod" |
| Namespace | import * as ns from "./mod" |
| Named type | import type { Foo } from "./mod" |
| Inline type | import { foo, type Foo } from "./mod" |
| Side effect | import "./mod" |
Export Forms
| Form | Example |
|---|---|
| Named value | export const x = 1 |
| Named type | export type Foo = ... |
| Default | export default class {} |
| Re-export | export { x } from "./mod" |
| Re-export all | export * from "./mod" |
| Type re-export | export type { Foo } from "./mod" |
Compiler Options
| Option | Effect |
|---|---|
module | Output format (esnext, commonjs, nodenext) |
moduleResolution | Resolver (bundler, node16, nodenext, node) |
verbatimModuleSyntax | Emit imports as written |
isolatedModules | Enforce per-file transpilation rules |
esModuleInterop | CommonJS/ESM compatibility |
allowSyntheticDefaultImports | Allow default imports of CJS |
Resolution Modes
| Mode | Extensions | exports | For |
|---|---|---|---|
bundler | Optional | โ | Bundled apps |
node16 / nodenext | Required | โ | Node.js |
node / node10 | Optional | โ | Legacy |
classic | Optional | โ | Legacy TypeScript |
Circular Imports
| Technique | Effect |
|---|---|
| Extract shared module | Breaks the cycle |
import type | Type-only, erased |
| Defer use to function call | Runtime cycle avoided |
madge, dpdm | Detect 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
| Pitfall | Problem | Solution |
|---|---|---|
| Type import not marked | Runtime error under verbatimModuleSyntax | Use import type |
Missing .js extension | Error under node16 | Add .js to relative imports |
.ts in import path | Compile error | Use .js |
| Circular import | Runtime undefined | Break with import type |
| Default export renamed | Not greppable | Use named exports |
Wrong moduleResolution | Resolution errors | Match the runtime |
esModuleInterop off | Awkward CJS imports | Enable it |
| Side-effect import removed | Effect missing | Set "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
| Item | Value |
|---|---|
| Value import | import { foo } from "./mod" |
| Default import | import foo from "./mod" |
| Namespace import | import * as ns from "./mod" |
| Type-only import | import type { Foo } from "./mod" |
| Inline type | import { foo, type Foo } from "./mod" |
| Side effect | import "./mod" |
| Re-export | export { x } from "./mod" |
| Re-export all | export * 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 typeand the inlinetypemodifier mark them, and they never appear in the emitted JavaScript verbatimModuleSyntaxmakes the emitted imports match the written imports โ the developer marks type imports, and the compiler does not elide themisolatedModulesaligns TypeScript with bundlers โ the option enforces rules that make each file independently transpilable, which is what esbuild, Vite, and Babel requiremoduleResolutiondecides how import paths resolve โbundleraccepts extensionless imports,node16andnodenextrequire.jsextensions, and the choice must match the runtime- The
.jsextension is required undernode16andnodenextโ the source imports the future.jsfile, not the.tsfile, 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
esModuleInteropenables 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
moduleResolutionor 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!