Back to TypeScript tutorials
Basic12 min read

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

Get In Touch


Ready to discuss your next project? Drop me a message.