Modules

10 questions found

TypeScript's import/export statements are (mostly) standard ES Module syntax with type-only extensions layered on top; the 'module' compiler option in tsconfig.json controls what actual JavaScript module format (CommonJS, ESNext, ES2020, etc.) this syntax gets compiled down to for the target runtime.
// source.ts (ES Module syntax)
export function add(a: number, b: number): number { return a + b; }

// compiled output, if 'module': 'commonjs'
// exports.add = function (a, b) { return a + b; };
Real-world example Choosing 'commonjs' as the module target for a Node.js backend versus 'esnext' for a modern bundler-based frontend.

Common follow-ups: How do you decide which 'module' setting to use for a given project type?

tsconfig & Compiler Options
'import type' explicitly imports only type information, which the compiler completely erases from the output JavaScript — useful for making it clear an import has zero runtime cost, and required in some strict module setups (isolatedModules) to disambiguate type-only imports.
import type { User } from './types';
import { fetchUser } from './api';

function display(user: User) { /* ... */ } // 'User' never exists in compiled JS output
Real-world example Clearly signaling in a large codebase which imports are purely for type checking versus which pull in actual runtime dependencies.

Common follow-ups: What happens if you accidentally use 'import type' for something you also need as a runtime value?

Declaration Files
Without esModuleInterop, importing a CommonJS module that exports a single function/value (via module.exports = x) using ES-style `import x from 'module'` syntax doesn't work correctly, because CommonJS and ES Modules have subtly incompatible default-export semantics; esModuleInterop adds compatibility shims so default imports 'just work' against CommonJS modules.
// Without esModuleInterop, this often fails or needs `import * as x`
// With esModuleInterop: true
import express from 'express'; // works cleanly, even though express uses module.exports
Real-world example Enabling clean default-import syntax for Node.js libraries like express, lodash, or moment that use CommonJS's module.exports.

Common follow-ups: What alternative, more verbose import syntax would you need without esModuleInterop enabled?

tsconfig & Compiler Options
The 'paths' option (used together with 'baseUrl') lets you define custom import aliases (like '@/components/*') that TypeScript resolves during type-checking — though note the bundler or runtime (Webpack, Vite, ts-node, Node itself) must be separately configured to understand the same aliases at build/run time, since TypeScript's path mapping is a compile-time-only convenience.
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": { "@components/*": ["src/components/*"] }
  }
}

// usage.ts
import Button from '@components/Button'; // instead of '../../../components/Button'
Real-world example Avoiding long, fragile relative import chains like '../../../../components/Button' in a deeply nested project structure.

Common follow-ups: Why doesn't configuring 'paths' alone make the aliases work at runtime in a plain Node.js script?

tsconfig & Compiler Options
isolatedModules ensures every file can be safely transpiled INDEPENDENTLY, one file at a time, without needing full cross-file type information — some fast, non-typechecking compilers (Babel, esbuild) work this way for speed, and certain TypeScript patterns (like re-exporting only a type without 'export type') break that single-file assumption, so this flag catches them at typecheck time.
// With isolatedModules: true, this needs 'export type' explicitly:
export type { User } from './types'; // OK
// export { User } from './types'; // Error if User is type-only and isolatedModules is on
Real-world example Ensuring a codebase remains compatible with a fast Babel or esbuild-based build pipeline that doesn't do full program-wide type analysis.

Common follow-ups: Does isolatedModules perform any type-checking itself, or is it purely a structural/syntax constraint?

tsconfig & Compiler Options
The underlying circular-dependency behavior is inherited directly from the target JavaScript module system's own semantics (partially-initialized exports at the moment of the cycle) — TypeScript's type system itself doesn't change this runtime behavior, though its static analysis can sometimes make circularly-typed interfaces work at the type level even when runtime values would be problematic.
// a.ts
import { b } from './b';
export const a = 'A';
console.log(b); // may be undefined at this point due to the cycle, regardless of TS

// b.ts
import { a } from './a';
export const b = 'B';
Real-world example Debugging a circular-import-caused undefined value bug that TypeScript's type checker didn't (and structurally couldn't) catch.

Common follow-ups: Why can't TypeScript's static type checker fully prevent this class of runtime circular-dependency bug?

ES Modules
Bundlers like Webpack or Vite let you import non-JS assets directly, which isn't valid in plain JS/TS module resolution — you provide a wildcard ambient module declaration describing what type that import resolves to (often a string for the asset URL, or a React component for SVGs configured that way), letting TypeScript accept these bundler-specific imports.
// assets.d.ts
declare module '*.svg' {
  const content: string;
  export default content;
}
declare module '*.css' {
  const styles: Record<string, string>;
  export default styles;
}

// usage.tsx
import logo from './logo.svg'; // typed as string
Real-world example Making a Webpack/Vite frontend project's non-JS asset imports (SVGs, CSS Modules, images) type-check correctly.

Common follow-ups: How would you type a CSS Modules import to get autocomplete on the specific class names it exports?

Declaration Files
moduleResolution controls the ALGORITHM TypeScript uses to search for a module's files/types given an import specifier (e.g. 'classic' vs 'node' vs 'bundler' strategies) — different settings walk node_modules, package.json 'exports' maps, or file-extension resolution differently, and mismatches here are a common source of 'Cannot find module' errors despite the package clearly being installed.
// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "bundler" // matches modern bundler resolution behavior closely
  }
}
Real-world example Fixing a 'Cannot find module' error that only appears in TypeScript despite the import working fine at runtime via the bundler.

Common follow-ups: Why was the 'bundler' moduleResolution setting introduced as an alternative to 'node' / 'node16'?

tsconfig & Compiler Options
Barrel files that re-export everything from many submodules make imports more convenient, but can hurt both bundler tree-shaking (some bundlers struggle to eliminate unused re-exported code through a barrel) and TypeScript compiler performance (the compiler must resolve and type-check the entire re-export graph even if a consumer only imports one specific named export).
// index.ts (barrel file)
export * from './Button';
export * from './Modal';
export * from './Tooltip';
// A consumer importing only { Button } may still pull in extra work/analysis for Modal and Tooltip
Real-world example Debugging why a bundle unexpectedly includes an entire component library when only one component was actually imported.

Common follow-ups: What alternative import strategies help mitigate these barrel-file downsides in a large component library?

ES Modules
Declare both `export default` and named `export`s normally in the module; how consumers import it depends on esModuleInterop and the module target — with esModuleInterop enabled, `import Default, { named } from 'module'` works cleanly, while under strict ES module interop rules without it, you may need `import * as mod from 'module'` and access `mod.default` explicitly.
// mathLib.ts
export default function add(a: number, b: number) { return a + b; }
export const PI = 3.14159;

// consumer.ts (with esModuleInterop: true)
import add, { PI } from './mathLib';
Real-world example Designing a utility library's public API to offer both a convenient default export and additional named utility exports.

Common follow-ups: What subtle bug can occur if a CommonJS-compiled module's default export handling doesn't match what a consumer's bundler expects?

ES Modules