Modules & Namespaces
Organize TypeScript code with ES modules, path resolution, and declaration files for JavaScript libraries.
ES Module Syntax in TypeScript
TypeScript supports `import`/`export` natively. Use `import type` for type-only imports that erase completely from output — important for bundler tree shaking and avoiding circular runtime dependencies.
Configure `moduleResolution: "bundler"` or `"node16"`/`"nodenext"` depending on your toolchain. Modern Next.js and Vite projects typically use bundler resolution with `module: "ESNext"`.
- `import type` for type-only imports
- Align moduleResolution with bundler
- ESM-first for new projects
import type { User } from './types';
import { fetchUser } from './api';Module Resolution
The compiler resolves imports using `baseUrl` and `paths` aliases in tsconfig. Align aliases with bundler config (webpack, vite, Next.js) so IDE and build agree on paths.
`moduleResolution: "node16"` respects package.json `exports` and requires explicit file extensions in some ESM setups. Read your runtime docs when imports fail at runtime but pass typecheck.
- `paths` aliases need bundler alignment
- Node16 resolution respects package exports
- Extension requirements in pure ESM
// tsconfig paths
"paths": { "@/*": ["./src/*"] }Declaration Files (.d.ts)
Declaration files describe types for JavaScript libraries without source TS. DefinitelyTyped (`@types/*`) packages ship community `.d.ts` files. Author `declare module "pkg"` when types are missing.
Enable `declaration: true` when publishing typed libraries so consumers import your public types. Use `export {}` to make a file a module rather than a global script.
- `.d.ts` for JS library types
- `@types/*` from DefinitelyTyped
- Publish declarations for libraries
declare module 'legacy-lib' {
export function parse(input: string): unknown;
}Namespaces (Legacy)
TypeScript namespaces (`namespace Foo {}`) predate ES modules and pollute global scope when used carelessly. Prefer ES modules for all new code — namespaces remain in legacy codebases and declaration merging scenarios.
`namespace` with `export` can wrap old global scripts during migration. Plan module-by-module conversion rather than indefinite namespace nesting.
- Namespaces are legacy — prefer modules
- Used in old code and some .d.ts files
- Migrate incrementally to import/export
Barrel Files and Public API
Index files re-export module public surfaces: `export * from './user'`. Control your package public API with explicit exports rather than wildcard re-exports that leak internals.
For libraries, document the supported import paths in README and enforce via package.json `exports`. Application code can use barrels sparingly — deep imports often tree-shake better.
- Explicit exports for library public API
- Barrel files trade convenience for bundle size
- Package.json exports enforce boundaries