Modules and tsconfig
In this chapter, you learn about TypeScript's module system and project configuration.
What you learn in this chapter
- How the module system (import/export) works
- Various export/import patterns
- Re-exports and barrel files
- The basics of tsconfig.json and how to configure it
- Important compilerOptions
By understanding the module system, you can split code by feature and build a reusable, maintainable project structure.
What is a module
A module is code split by feature and bundled into a reusable unit.
Benefits of modules:
- Organizing and structuring code
- Separating namespaces (preventing variable collisions)
- Improving reusability
- Improving maintainability
Modules in TypeScript:
- Make something public with the
exportkeyword - Load another module with the
importkeyword - A file is treated as a module if it has an
importorexport
Export (export)
// user.ts - named exports
// Export an interface: make the User type usable from the outside
export interface User {
id: number;
name: string;
email: string;
}
// Export a class: make the UserService class usable from the outside
export class UserService {
// private: accessible only from inside this class
private users: User[] = [];
// A method that adds a user
addUser(user: User): void {
this.users.push(user);
}
// A method that finds a user by ID
getUserById(id: number): User | undefined {
// find: returns the first element that matches the condition
return this.users.find(u => u.id === id);
}
}
// Export a function
export function formatUserName(user: User): string {
return `${user.name} (ID: ${user.id})`;
}
// Export a constant
export const MAX_USERS = 100;
Code walkthrough:
- Make something public with the
exportkeyword - You can export interfaces, classes, functions, constants, and so on
- You can export multiple items from the same file
Import (import)
// main.ts - named imports
// Import specific items
// Surround the items to import with { }
import { User, UserService } from "./user";
// Create an instance of the UserService class
const service = new UserService();
// Create an object using the User type
const user: User = {
id: 1,
name: "Yamada Taro",
email: "yamada@example.com"
};
// Call the method to add a user
service.addUser(user);
Code walkthrough:
- Surround the items to import with
{} - Specify the module path after
from(the extension can be omitted) - Use a relative path (
./,../) or an absolute path
Various export patterns
// ===== Export patterns =====
// Pattern 1: export individually (export at the same time as the declaration)
export class Product {}
export interface ProductData {}
// Pattern 2: export all together at the end
class Order {}
interface OrderData {}
const TAX_RATE = 0.1;
// Export multiple items at once
export { Order, OrderData, TAX_RATE };
// Pattern 3: export using an alias
class InternalUser {}
// Export InternalUser under the name User
export { InternalUser as User };
// Pattern 4: a default export (only one per file)
// Use it for the "main" item of this module
export default class Config {
apiUrl: string = "https://api.example.com";
}
Various import patterns
// ===== Import patterns =====
// Pattern 1: import specific items
import { Product, ProductData } from "./product";
// Pattern 2: import using an alias
// Avoid name collisions or change to a clearer name
import { Order as PurchaseOrder } from "./order";
// Pattern 3: import everything (as a namespace)
// Get all exports as an object with * as
import * as ProductModule from "./product";
const product = new ProductModule.Product();
// Pattern 4: import a default export
// No {} needed; you can give it any name
import Config from "./config";
// Pattern 5: import a default and named exports at the same time
import Config, { Helper, Utility } from "./config";
// Pattern 6: import only the type (TypeScript 3.8 and later)
// Type information only; it is not included in the compiled JavaScript
import type { User } from "./user";
Code walkthrough:
- Named exports require
{} - Default exports do not need
{}, and you can give them any name - The
typekeyword imports only the type (it is not included in the compiled code)
Recommendations:
- Prefer named exports: IDE completion works well, and refactoring is easy
- Use a default export only when a file's main feature is a single one
Re-exports (barrel exports)
A way to expose multiple modules from a single entry point.
// models/index.ts - a barrel file
// Expose multiple modules from a single file
// Choose specific items to re-export
export { User, UserService } from "./user";
export { Product, ProductService } from "./product";
export { Order, OrderService } from "./order";
// Or re-export everything at once
// Re-export all exports of the target modules
export * from "./user";
export * from "./product";
export * from "./order";
// main.ts - import everything at once from the barrel file
import { User, Product, Order } from "./models";
// No need to import individually from ./models/user, ./models/product, ./models/order
Benefits of a barrel file:
- Simplifies import statements
- Makes it easy to change the module structure
- Makes it easier to control the public API
The difference between modules and scripts
// module.ts - a module (has import/export)
export function greet() {
console.log("Hello from module!");
}
// The top-level variables in this file are not visible from the outside (module scope)
const secret = "module secret";
// script.ts - a script (no import/export)
function greet() {
console.log("Hello from script!");
}
// The top-level variables in this file are in the global scope
// They may be visible from other files
const globalVar = "accessible everywhere";
Summary of the differences:
| Feature | Module | Script |
|---|---|---|
| import/export | Yes | No |
| Scope | Its own scope | Global scope |
| Recommendation | Recommended | Not recommended |
A module is safer and more maintainable.
tsconfig.json - compiler settings
Creating tsconfig.json
# Auto-generate tsconfig.json (a default config with comments is generated)
tsc --init
Basic structure
{
"compilerOptions": {
// Compiler options (configure TypeScript's behavior)
},
"include": [
"src/**/*" // files to compile (everything under the src folder)
],
"exclude": [
"node_modules", // folders to exclude
"**/*.spec.ts" // file patterns to exclude (test files)
]
}
Important compilerOptions
Basic options
{
"compilerOptions": {
// The target JavaScript version
// Which version the compiled JavaScript will be
"target": "ES2020",
// The module system
// ESNext: ES Modules (import/export)
// commonjs: for Node.js (require/module.exports)
"module": "ESNext",
// Library files (the type definitions to use)
// ES2020: the ES2020 standard library
// DOM: type definitions for browser APIs
"lib": ["ES2020", "DOM"],
// The output directory (where the compiled files are saved)
"outDir": "./dist",
// The root directory (where the source files are)
"rootDir": "./src",
// Generate source maps (for debugging)
// .map files are generated, and the debugger can show the TypeScript code
"sourceMap": true,
// Remove comments (strip comments from the output files)
"removeComments": true,
// Do not emit on error (do not generate JS files if there is a type error)
"noEmitOnError": true
}
}
Description of each option:
| Option | Description | Recommended value |
|---|---|---|
| target | The compiled JS version | ES2020 or higher |
| module | The module system format | ESNext |
| outDir | The output directory | ./dist |
| rootDir | The source root | ./src |
| sourceMap | Generate maps for debugging | true |
Type checking options (strict-related)
{
"compilerOptions": {
// Enable all strict options (recommended)
// Enabling this enables all of the following options
"strict": true,
// The following are enabled automatically by "strict": true:
// "noImplicitAny": true, // forbid the implicit any type
// "strictNullChecks": true, // stricter null checking
// "strictFunctionTypes": true, // stricter function type checking
// "strictBindCallApply": true, // stricter bind/call/apply checking
// "strictPropertyInitialization": true, // check class property initialization
// "noImplicitThis": true, // forbid the implicit type of this
// "alwaysStrict": true, // emit "use strict"
// Additional checks (not included in strict, so set them individually)
"noUnusedLocals": true, // warn about unused local variables
"noUnusedParameters": true, // warn about unused parameters
"noImplicitReturns": true, // forbid implicit returns
"noFallthroughCasesInSwitch": true // forbid fall-through in switch statements
}
}
We recommend enabling "strict": true. It improves type safety and helps you catch bugs early.
Module resolution options
{
"compilerOptions": {
// The module resolution method
// "bundler": when using a bundler like Vite / esbuild / Webpack (recommended)
// "nodenext": when running directly with Node.js
// "node": for legacy compatibility (not recommended for new projects)
"moduleResolution": "bundler",
// The base URL (for absolute path imports)
"baseUrl": "./",
// Path mapping (path aliases)
// Replace long paths with short aliases
"paths": {
"@/*": ["src/*"], // @/xxx → src/xxx
"@components/*": ["src/components/*"], // @components/xxx → src/components/xxx
"@utils/*": ["src/utils/*"] // @utils/xxx → src/utils/xxx
},
// Improve interoperability between ES Modules and CommonJS
"esModuleInterop": true,
// Allow default imports
"allowSyntheticDefaultImports": true
}
}
How to choose moduleResolution (2026)
| Value | Use | Typical environment |
|---|---|---|
"bundler" | A SPA or library using a bundler like Vite / esbuild / Webpack | React / Vue / Svelte projects |
"nodenext" | A CLI or server run directly with Node.js | Express / NestJS / CLI tools |
"node16" | Node.js 16+ compatible behavior (similar to nodenext but fixed) | Libraries that need compatibility |
"node" | For legacy compatibility. Do not choose for new projects | Only when migrating from an old project |
"bundler" is the most relaxed and allows imports that include .ts / .tsx extensions (combined with allowImportingTsExtensions: true). When running directly with Node.js, the extension and "type": "module" specifications become stricter, so choose "nodenext".
// An example import using paths
// A normal path (tends to get long)
import { Button } from "../../../components/Button";
// Using a path alias (short and clear)
import { Button } from "@components/Button"; // src/components/Button
import { formatDate } from "@utils/date"; // src/utils/date
A practical tsconfig.json example
{
"compilerOptions": {
// Basic settings
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
// Output settings
"outDir": "./dist",
"rootDir": "./src",
"sourceMap": true,
"removeComments": true,
"noEmitOnError": true,
// Type checking (strict mode)
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// Module resolution
"moduleResolution": "bundler",
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
// Modern TypeScript settings (TS 5.x)
"verbatimModuleSyntax": true, // preserve the interpretation of import/export as written (TS 5.0+)
"erasableSyntaxOnly": true, // forbid TS-specific syntax other than types (TS 5.8+)
"isolatedModules": true, // make it compilable on a per-file basis
"allowImportingTsExtensions": true, // allow imports with .ts / .tsx extensions
// Other
"skipLibCheck": true, // skip type checking of libraries (faster)
"forceConsistentCasingInFileNames": true // strictly check file name casing
},
// Compilation targets
"include": [
"src/**/*"
],
// Excluded files
"exclude": [
"node_modules",
"dist",
"**/*.spec.ts",
"**/*.test.ts"
]
}
Modern tsconfig settings (TS 5.x)
Here we explain the compiler options recommended for new projects as of 2026.
verbatimModuleSyntax (TS 5.0+)
An option that emits import/export exactly as written. To import only a type, you must write import type { ... } explicitly, which makes it clear from the code whether you are importing only a type or also need the value at runtime.
// With verbatimModuleSyntax: true, type-only imports must be explicit
// ❌ Error: User is only used as a type
import { User, fetchUser } from './user'
// ✅ OK: make the type import explicit
import type { User } from './user'
import { fetchUser } from './user'
// Or an inline type import
import { type User, fetchUser } from './user'
erasableSyntaxOnly (TS 5.8+)
An option that allows only types that can be erased, that is, types that still work as JavaScript if you just remove the type annotations. It forbids TypeScript-specific syntax that generates runtime code, such as enum, namespace, and constructor parameter properties.
// With erasableSyntaxOnly: true, the following are errors
// ❌ enum generates runtime code (an object)
enum Color { Red, Green, Blue }
// ❌ constructor parameter properties auto-generate field assignments
class User {
constructor(public name: string) {}
}
// ✅ Write these explicitly
const Color = { Red: 0, Green: 1, Blue: 2 } as const
type Color = typeof Color[keyof typeof Color]
class User {
name: string
constructor(name: string) {
this.name = name
}
}
erasableSyntaxOnly is an option for aligning with the constraints of type stripping (a method that only removes type annotations; available without a flag by default in Node.js 22.18 and later / 24) when Node.js runs .ts files directly. Non-erasable syntax like enum produces an ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX error when run directly with Node.js. Note that Bun / Deno run TypeScript with a full transpile rather than type stripping, so enum works too. Still, if you align with erasableSyntaxOnly, the same code runs as is on any of the Node.js / Bun / Deno runtimes.
import attributes (TS 5.3+)
A syntax for explicitly marking the module type when importing JSON or other module types. The previous assert { type: "json" } has been deprecated, and with { type: "json" } is now the standard.
// The official syntax in TS 5.3+
import data from './config.json' with { type: 'json' }
watch mode
# Watch for file changes and compile in real time
tsc --watch
# Or the short form
tsc -w
How watch mode behaves:
- Automatically detects file changes
- Immediately recompiles the changed file
- Displays any errors in the console
Keeping watch mode running during development lets you see the type-checking results in real time while you write code.
Try it: optimize tsconfig.json ★★
Create a tsconfig.json that meets the following requirements.
Requirements:
- Target ES2020
- Enable strict mode
- Make it possible to reference
src/with the@/path alias - Set the output destination to
dist/ - Enable source maps
- Warn about unused variables and parameters
Hint
- Set
targetto"ES2020" - Set
stricttotrue - Set
baseUrlandpathsto enable the path alias - Set
outDirto"./dist" - Set
sourceMaptotrue - Set
noUnusedLocalsandnoUnusedParameterstotrue
Answer and explanation
{
"compilerOptions": {
// Set the target to ES2020
"target": "ES2020",
// The module system (ES Modules)
"module": "ESNext",
"moduleResolution": "bundler",
// The output directory
"outDir": "./dist",
"rootDir": "./src",
// Enable source maps (for debugging)
"sourceMap": true,
// Enable strict mode (maximize type safety)
"strict": true,
// Warn about unused code
"noUnusedLocals": true,
"noUnusedParameters": true,
// Configure the path alias
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
},
// Other recommended settings
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Explanation:
strict: trueenables all the strict type checks- The path alias
@/*enables notation likeimport { xxx } from "@/components/xxx" sourceMap: truelets you debug TypeScript code directly in the browser's or VSCode's debuggernoUnusedLocalsandnoUnusedParametersdisplay a warning when there are unused variables or parameters
Summary
What you learned in this chapter:
- The module system: split and reuse code with import/export
- Various export patterns: named, default, re-export
- Various import patterns: named, alias, namespace, type-only
- Barrel files: expose multiple modules from a single entry point
- tsconfig.json: the configuration file for a TypeScript project
- Important compilerOptions: target, strict, paths, sourceMap, and so on
In the next chapter, you will learn about the development toolchain, including ESLint, Prettier, and Jest.
Common pitfalls for beginners
Common mistakes
❌ Confusing relative paths with absolute paths
// ❌ Using @ without a path alias configuration
import { User } from "@/types/user"; // Error: the module cannot be found
// ✅ Use a relative path, or configure paths
import { User } from "./types/user"; // a relative path
// Or configure it in tsconfig.json
// "paths": { "@/*": ["src/*"] }
Cause: A path alias like @/ requires the paths setting in tsconfig.json.
Solution: Use a relative path, or configure baseUrl and paths.
❌ Confusing default exports with named exports
// user.ts
export default class User {}
export const MAX_USERS = 100;
// ❌ Importing a default export with {}
import { User } from "./user"; // Error: User is not a named export
// ✅ A default does not need {}
import User from "./user";
// Or together with a named export
import User, { MAX_USERS } from "./user";
Cause: A default export is imported without {}.
Solution: Use import Xxx for default, and import { Xxx } for named.
❌ Confusing module and moduleResolution
{
"compilerOptions": {
// module: the module format to output
"module": "ESNext",
// moduleResolution: the module resolution method (the search algorithm)
"moduleResolution": "bundler"
}
}
Cause: module specifies the output format, while moduleResolution specifies the module search method.
Solution: For modern projects, we recommend "moduleResolution": "bundler" (when using a bundler) or "node16"/"nodenext" (when running directly with Node.js).
❌ Not understanding the effect of strict: true
// When strict is false, the following are allowed
let name; // implicit any
const user = { name: "Taro" };
user.age; // access to a non-existent property
// With strict: true, these become errors
Cause: strict: true enables several strict checks at once.
Solution: Always set strict: true in new projects to maximize type safety.
❌ Creating a circular reference with a barrel file
// models/index.ts (a barrel file)
export * from "./user";
export * from "./order";
// models/user.ts
import { Order } from "./index"; // import via the barrel
// If this is importing user from order.ts, it becomes a circular reference!
Cause: Importing via a barrel file can cause a circular reference. Solution: Import directly within the same module, and use the barrel as the external-facing API.