Skip to main content

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
info

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 export keyword
  • Load another module with the import keyword
  • A file is treated as a module if it has an import or export

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 export keyword
  • 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 type keyword 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:

FeatureModuleScript
import/exportYesNo
ScopeIts own scopeGlobal scope
RecommendationRecommendedNot 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:

OptionDescriptionRecommended value
targetThe compiled JS versionES2020 or higher
moduleThe module system formatESNext
outDirThe output directory./dist
rootDirThe source root./src
sourceMapGenerate maps for debuggingtrue
{
"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
}
}
warning

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
}
}
info

How to choose moduleResolution (2026)

ValueUseTypical environment
"bundler"A SPA or library using a bundler like Vite / esbuild / WebpackReact / Vue / Svelte projects
"nodenext"A CLI or server run directly with Node.jsExpress / 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 projectsOnly 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
}
}
info

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:

  1. Automatically detects file changes
  2. Immediately recompiles the changed file
  3. 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:

  1. Target ES2020
  2. Enable strict mode
  3. Make it possible to reference src/ with the @/ path alias
  4. Set the output destination to dist/
  5. Enable source maps
  6. Warn about unused variables and parameters
Hint
  1. Set target to "ES2020"
  2. Set strict to true
  3. Set baseUrl and paths to enable the path alias
  4. Set outDir to "./dist"
  5. Set sourceMap to true
  6. Set noUnusedLocals and noUnusedParameters to true
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: true enables all the strict type checks
  • The path alias @/* enables notation like import { xxx } from "@/components/xxx"
  • sourceMap: true lets you debug TypeScript code directly in the browser's or VSCode's debugger
  • noUnusedLocals and noUnusedParameters display 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

warning

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.