Skip to main content

Modern Tooling: tsconfig, Bundlers, ESM

You've written TypeScript code. Now you need to ship it. Unlike Python where you run .py files directly, TypeScript requires configuration decisions: Which JavaScript version should it compile to? Should files use modern ES Modules or legacy CommonJS? How do you compile fast enough that development doesn't become painful?

These questions matter because the TypeScript ecosystem evolved rapidly. Projects from 2020 use different patterns than projects started in 2025. When you inherit code or follow tutorials, you'll encounter conflicting configurations. Understanding the tooling lets you make informed choices instead of copying settings blindly.

This lesson covers the three essential pieces: configuring TypeScript with tsconfig.json, choosing between module systems, and using modern build tools that compile 100x faster than the default TypeScript compiler.

The tsconfig.json File

Every TypeScript project needs a tsconfig.json file. It tells the compiler how to process your code. Think of it like Python's pyproject.toml—it configures project behavior.

Creating Your First Configuration

Create a new project directory and initialize TypeScript:

mkdir ts-project && cd ts-project
npm init -y
npm install typescript --save-dev
npx tsc --init

Output:

Created a new tsconfig.json with:
target: es2016
module: commonjs
strict: true
esModuleInterop: true
...

This generates a tsconfig.json with many options. Most are commented out. Here's a minimal configuration for modern Node.js projects:

{
"compilerOptions": {
"target": "ES2024",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

Understanding Key Options

Each option controls how TypeScript processes your code:

OptionPurposeRecommendation
targetJavaScript version to compile toES2024 for Node.js 22+, ES2020 for broader support
moduleModule system for outputNodeNext for Node.js, ESNext for bundlers
moduleResolutionHow imports are resolvedMatch your module setting
strictEnable all strict type checksAlways true for new projects
esModuleInteropBetter CommonJS/ESM interopAlways true
skipLibCheckSkip type checking in node_modulestrue for faster compilation
outDirWhere compiled JavaScript goes./dist is conventional
rootDirWhere source TypeScript lives./src is conventional

The Strict Mode Decision

The strict option enables multiple type-checking flags at once:

{
"compilerOptions": {
"strict": true
// This enables:
// - strictNullChecks: null and undefined are distinct types
// - strictFunctionTypes: function parameter types are checked
// - strictBindCallApply: bind, call, apply are type-checked
// - strictPropertyInitialization: class properties must be initialized
// - noImplicitAny: variables must have explicit types
// - noImplicitThis: 'this' must have explicit type
// - alwaysStrict: emit "use strict" in JavaScript
}
}

Always enable strict mode for new projects. It catches bugs at compile time instead of runtime. The errors feel annoying at first, but they prevent real problems.

Testing Your Configuration

Create a source file to verify the configuration works:

// src/index.ts
interface User {
id: number;
name: string;
email: string;
}

function greetUser(user: User): string {
return `Hello, ${user.name}!`;
}

const user: User = { id: 1, name: "Alice", email: "alice@example.com" };
console.log(greetUser(user));

Compile with the TypeScript compiler:

npx tsc

Check the output:

cat dist/index.js

Output:

"use strict";
function greetUser(user) {
return `Hello, ${user.name}!`;
}
const user = { id: 1, name: "Alice", email: "alice@example.com" };
console.log(greetUser(user));

The types disappear—they're only for compile-time checking. The output is plain JavaScript that Node.js runs directly.

ES Modules vs CommonJS

JavaScript has two module systems. Understanding both is essential because you'll encounter both in real projects.

CommonJS (Legacy)

CommonJS was Node.js's original module system. You'll see it in older code:

// math.js (CommonJS)
function add(a, b) {
return a + b;
}

module.exports = { add };
// app.js (CommonJS)
const { add } = require('./math');
console.log(add(2, 3)); // 5

Characteristics:

  • Uses require() and module.exports
  • Synchronous loading
  • Default in Node.js until recently
  • File extension: .js or .cjs

ES Modules (Modern Standard)

ES Modules (ESM) are the JavaScript standard. Use this for new projects:

// math.ts (ESM)
export function add(a: number, b: number): number {
return a + b;
}
// app.ts (ESM)
import { add } from './math.js'; // Note: .js extension even for .ts files
console.log(add(2, 3)); // 5

Characteristics:

  • Uses import and export
  • Can be asynchronous (dynamic imports)
  • Works in browsers natively
  • File extension: .js, .mjs, or .ts

Why ESM is the Future

FeatureCommonJSESM
Browser supportNo (requires bundler)Yes (native)
Static analysisLimitedExcellent (tree-shaking)
Top-level awaitNoYes
Spec standardNode.js specificJavaScript standard
Future supportMaintenance modeActive development

Configuring Node.js for ESM

To use ESM in Node.js, add "type": "module" to package.json:

{
"name": "ts-project",
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}

With this setting, Node.js treats .js files as ES Modules by default.

Fast Compilation with esbuild

The TypeScript compiler (tsc) is thorough but slow. It type-checks and compiles. For large projects, compilation takes seconds or minutes.

esbuild is 100x faster because it only transforms code—it skips type checking. Use tsc for type checking, esbuild for building.

Installing esbuild

npm install esbuild --save-dev

Compiling a Single File

npx esbuild src/index.ts --bundle --outfile=dist/index.js --platform=node

Output:

  dist/index.js  142b

⚡ Done in 2ms

Compare to tsc which typically takes 500ms-2000ms for the same file.

esbuild Options Explained

OptionPurpose
--bundleInclude all dependencies in output
--outfileSingle output file
--outdirOutput directory (multiple files)
--platform=nodeTarget Node.js (vs browser)
--format=esmOutput ES Modules
--minifyCompress output for production
--sourcemapGenerate source maps for debugging

Development Workflow

For development, combine both tools:

{
"scripts": {
"typecheck": "tsc --noEmit",
"build": "esbuild src/index.ts --bundle --outfile=dist/index.js --platform=node --format=esm",
"dev": "esbuild src/index.ts --bundle --outfile=dist/index.js --platform=node --format=esm --watch",
"start": "node dist/index.js"
}
}
  • typecheck: Verify types without generating JavaScript
  • build: Fast compilation for production
  • dev: Watch mode—rebuilds on file changes (instant)
  • start: Run the compiled JavaScript

Run type checking separately:

npm run typecheck && npm run build

Output:

> tsc --noEmit
# (no output means no type errors)

> esbuild src/index.ts --bundle --outfile=dist/index.js --platform=node --format=esm
dist/index.js 142b
⚡ Done in 3ms

Running TypeScript Directly

Node.js 22+ can run TypeScript files directly without any compilation step. This is experimental but increasingly practical.

Using --experimental-strip-types

node --experimental-strip-types src/index.ts

Output:

Hello, Alice!

Node.js strips the types at runtime, similar to what esbuild does. You get instant execution without a build step.

Limitations

Type stripping has constraints:

  • No enums: Use const objects or string unions instead
  • No namespaces: Use ES Modules
  • No decorators with emitDecoratorMetadata: Legacy decorator syntax not supported
  • No const enums: Regular enums also unsupported

For AI engineering work, these limitations rarely matter. The features you need (interfaces, type annotations, generics) all work.

When to Use Direct Execution

Use CaseApproach
Quick scriptsnode --experimental-strip-types
Developmentesbuild with watch mode
Productionesbuild bundled output
Librariestsc for declaration files

Vite for Browser Projects

When building browser-based AI interfaces, Vite provides the fastest development experience.

Setting Up a Vite Project

npm create vite@latest ai-chat-ui -- --template react-ts
cd ai-chat-ui
npm install
npm run dev

Output:

  VITE v6.x.x  ready in 200 ms

➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help

Vite features:

  • Instant server start: No bundling during development
  • Hot Module Replacement: Changes appear without page refresh
  • TypeScript built-in: No configuration needed
  • Optimized builds: esbuild for dependencies, Rollup for production

Vite Configuration

Vite works out of the box, but you can customize vite.config.ts:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
plugins: [react()],
build: {
target: 'es2022',
sourcemap: true
},
server: {
port: 3000
}
})

For AI chat interfaces that stream responses, Vite's fast refresh makes development feel instantaneous.

Comparing Python and TypeScript Tooling

Coming from Python, here's how the tools map:

PythonTypeScriptPurpose
pyproject.tomltsconfig.jsonProject configuration
pip / uvnpm / pnpmPackage management
venvnode_modulesDependency isolation
python script.pynode --experimental-strip-types script.tsDirect execution
mypytsc --noEmitType checking
ruff / blackprettier / eslintFormatting and linting

The concepts transfer directly. The main difference is TypeScript has more compilation options because JavaScript evolved with multiple module systems.

Common Configuration Patterns

Node.js Backend (API Server)

{
"compilerOptions": {
"target": "ES2024",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

Browser Library (npm Package)

{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

React/Vite Frontend

{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

Notice that frontend configs often use "noEmit": true because Vite handles the actual compilation. TypeScript only provides type checking.

Troubleshooting Common Issues

Issue: "Cannot find module" errors

Your module resolution doesn't match your imports:

{
"compilerOptions": {
"moduleResolution": "NodeNext" // For Node.js 16+
// OR
"moduleResolution": "bundler" // For Vite/webpack
}
}

Issue: "Relative imports must include file extensions"

With "module": "NodeNext", you must include extensions:

// Wrong
import { add } from './math';

// Correct
import { add } from './math.js'; // .js even for .ts files

Issue: esbuild doesn't catch type errors

esbuild only transforms code. Run tsc --noEmit separately:

npm run typecheck && npm run build

Issue: Node.js doesn't recognize TypeScript

Ensure you're using Node.js 22+ and the experimental flag:

node --version  # Should be v22.x.x or higher
node --experimental-strip-types src/index.ts

Try With AI

Prompt 1: Configuration Review

Review this tsconfig.json for a Node.js 22+ backend project:

{
"compilerOptions": {
"target": "ES5",
"module": "CommonJS",
"strict": false
}
}

What should I change for a modern setup? Explain why each change matters.

What you're learning: How to evaluate TypeScript configurations and understand the reasoning behind each option

Prompt 2: Build Tool Selection

I'm building an AI chat interface that streams responses token-by-token.
Users interact through a React frontend, and I need fast development iteration.

Should I use:
1. tsc alone
2. esbuild
3. Vite
4. Node.js direct execution

Explain which tool fits this use case and why.

What you're learning: Matching build tools to project requirements based on use case analysis

Prompt 3: Migration Exercise

I have a Python project structure:
- src/
- main.py
- utils/
- helpers.py
- tests/
- pyproject.toml
- requirements.txt

Create an equivalent TypeScript project structure with:
- tsconfig.json
- package.json with scripts
- Appropriate directory layout

Show me the configuration files and explain what each does.

What you're learning: Translating Python project patterns to TypeScript equivalents, reinforcing the conceptual mapping between ecosystems