TypeScript Modules
Learn how to organize TypeScript code using ES modules, named exports, and default exports.
TL;DR
- 01Use export and import to share code between files.
- 02Choose named exports for multiple values per module.
- 03Use type-only imports to keep runtime bundles small.
Tips
- 01Prefer named exports over default exports, since they keep import names consistent and make refactoring much safer.
Warnings
- 01Path aliases require both <code>tsconfig.json</code> and your bundler to agree, or imports will work in the IDE but fail at runtime.
Named Exports
- Add the
exportkeyword in front of any declaration to share it.// utils.ts export const PI = 3.14159; export function double(n: number) { return n * 2; } - Import named exports using curly braces and the exact names.
import { PI, double } from "./utils"; - Rename imports inline with the
askeyword to avoid conflicts.import { double as multiplyByTwo } from "./utils"; - Export multiple values from one file for related helpers and constants.
- This is the most common style in modern TypeScript codebases.
Default Exports
- Use
export defaultto mark one main export per file.// logger.ts export default function log(msg: string) { console.log(msg); } - Import the default with any name you choose, without curly braces.
import log from "./logger"; - A file can have one default export and any number of named exports.
- Default exports lose the original name during import, which hurts refactoring.
- Many style guides recommend named exports over defaults for that reason.
Re-exports
- Re-export from another file to build a barrel that groups related modules.
// index.ts export { double, PI } from "./utils"; export { default as log } from "./logger"; - Use
export * fromto re-export every named export from a module.export * from "./utils"; - Barrels make imports cleaner across the rest of your codebase.
- They can also hurt tree-shaking, so use them carefully in large projects.
- Place barrel files at the root of folders for predictable import paths.
Type-only Imports
- Use the
typekeyword to import only types, not runtime values.import type { User } from "./types"; - Type-only imports are erased during compilation for smaller bundles.
- Mix value and type imports in one statement using the inline
typekeyword.import { type User, getUser } from "./api"; - Use
export typeto re-export types without runtime code.export type { User } from "./types"; - This is especially helpful when working with bundlers and isolated modules.
Module Resolution
- Use relative paths like
./utilsfor files inside your project. - Use bare specifiers like
reactfor packages installed innode_modules. - Configure path aliases in
tsconfig.jsonto shorten deep import paths.import { Button } from "@/components/Button"; - The
moduleResolutionsetting controls how the compiler finds modules. - Use
"bundler"mode in modern projects that rely on tools like Vite or Webpack.
FAQ
Named exports let you export multiple values from one file and must be imported using their exact name (or an alias). Default exports allow only one per file and can be imported under any name, which makes them harder to track during refactoring.
Use export * from './module' to re-export all named exports, or export { foo, bar } from './module' to selectively re-export. This is common in index files to create a clean public API for a directory.
Use import type { Foo } from './foo' whenever you only need a type for annotations or generics—TypeScript erases it at compile time, so it never appears in the emitted JavaScript. This keeps bundle size smaller and avoids circular-dependency issues caused by value imports.
TypeScript path aliases in tsconfig.json only affect type-checking; bundlers like webpack, Vite, or esbuild need their own alias configuration to resolve them at runtime. Add matching aliases in your bundler config (e.g., resolve.alias in Vite) to make both environments agree.
You can export both simultaneously: export const Status = { Active: 'active' } as const; export type Status = typeof Status[keyof typeof Status];. Consumers can then import the runtime value and the type from the same path without conflict.