TypeScript uses ES module syntax (import/export) with added features — import type ensures type-only imports are erased at runtime, module resolution strategies (node16, bundler) control how imports are resolved, and verbatimModuleSyntax enforces explicit type import annotations.
Explicitly marks type-only imports for erasure at compile time — essential for single-file transpilers (esbuild, SWC) that can't do cross-file analysis.
Requires explicit import type/export type annotations — ensures every import in output JavaScript is intentional, no accidental side-effect imports.
node16 for Node.js (requires .js extensions), bundler for frontend tools (extensionless), node for legacy CJS. Determines how imports map to files.
tsconfig paths map import specifiers to file paths (@/* → ./src/*), but the runtime/bundler needs its own corresponding alias configuration.
TypeScript's module system builds on JavaScript's ES modules with additional type-specific features. Understanding module resolution, type-only imports, and configuration is essential for structuring TypeScript projects.
TypeScript uses standard ES module syntax — the same as modern JavaScript:
// Named exports
export function validate(input: string): boolean { ... }
export interface Config { port: number; host: string; }
// Named imports
import { validate, Config } from './validation';
// Default export/import
export default class App { ... }
import App from './app';
// Namespace import
import * as utils from './utils';A file with any import or export statement is a module. Files without imports/exports are scripts — their declarations are global.
import type explicitly marks imports as type-only — they're guaranteed to be erased at compile time:
import type { User, Config } from './types';
import { processUser, type ApiResponse } from './api';Why this matters: bundlers like esbuild and SWC perform single-file transpilation — they can't determine if an import is only used as a type across the whole project. import type tells the transpiler definitively that the import is type-only and should be removed.
TypeScript 5.0 introduced verbatimModuleSyntax (replacing isolatedModules + importsNotUsedAsValues). When enabled, it requires explicit import type for type-only imports and export type for type-only exports. This ensures every import statement in the output JavaScript is intentional — no surprise import side effects, no reliance on cross-file analysis.
This is the recommended setting for projects using modern bundlers.
Module resolution controls how TypeScript finds the file behind an import specifier. Key strategies:
.js, not .ts). Respects exports in package.json. The standard for Node.js projects.exports in package.json. Best for frontend projects using a bundler..ts, .tsx, .js, /index.ts, etc. Doesn't support package.json exports.The node16 strategy notably requires .js extensions in relative imports even though the source files are .ts — this matches Node.js runtime behavior where the compiled .js files are what actually resolve.
paths in tsconfig maps import specifiers to file paths:
{ "paths": { "@/*": ["./src/*"], "@components/*": ["./src/components/*"] } }Path aliases require a corresponding configuration in your bundler/runtime (webpack resolve.alias, Vite resolve.alias, or --loader in Node.js). TypeScript resolves types using paths, but the runtime needs its own mapping.
TypeScript handles the ESM/CJS interop:
esModuleInterop: true enables default imports from CommonJS modules (import React from 'react' instead of import * as React from 'react')allowSyntheticDefaultImports allows the syntax without emitting interop helpersMost projects enable esModuleInterop — it's included in strict recommendations.
TypeScript uses ES module syntax with import type for compile-time-only imports. verbatimModuleSyntax enforces explicit type annotations so bundlers can safely remove type imports. Module resolution strategies (node16, bundler) determine how import specifiers map to files — choose based on your runtime/bundler. Path aliases need both tsconfig and bundler configuration.
Fun Fact
The node16 resolution strategy requires .js extensions in TypeScript imports (e.g., import { foo } from './utils.js' in a .ts file), which confuses many developers — why reference a .js file that doesn't exist yet? The answer is that TypeScript resolves based on the output file paths, not the source files. The .ts file will compile to .js, and at runtime Node.js needs the .js extension.