A Modern Guide to TypeScript: Modules, Tooling, and Configuration
Understanding the TypeScript ecosystem and setting up your project for success
If you’ve been working with TypeScript, you’ve probably encountered confusing choices about module systems, compilers, and configuration options. In this guide, I’ll break down the essential concepts and help you make informed decisions for your TypeScript projects.
Understanding Module Systems: CJS vs ESM
One of the first decisions you’ll face is choosing between CommonJS (CJS) and ECMAScript Modules (ESM). While both get the job done, ESM is the way forward for modern JavaScript and TypeScript projects.
Here’s the key difference:
- CJS uses synchronous imports (
require()), which was the original Node.js module system - ESM uses asynchronous imports (
import), which is the JavaScript standard
ESM brings modern features like top-level await, making your code cleaner and more aligned with the JavaScript ecosystem. If you’re starting a new project, go with ESM—it’s future-proof.
The TypeScript Tooling Landscape: tsx, ts-node, and tsc
Let’s demystify the three main tools you’ll encounter in the TypeScript world.
TypeScript Runners: tsx vs ts-node
Both tsx and ts-node let you run TypeScript files directly without manual compilation:
npx ts-node src/index.ts
npx tsx src/index.ts
ts-node is the veteran here. It compiles TypeScript in memory using tsc under the hood. It’s mature and widely adopted, but comes with two downsides: it’s relatively slow and doesn’t play nicely with ESM.
tsx is the new kid on the block, and it’s built on esbuild. The result? Lightning-fast execution that handles ESM beautifully. For modern development, tsx is the clear winner.
The TypeScript Compiler: tsc
While tsx and ts-node are great for running code, tsc is the official TypeScript compiler that transforms your .ts files into JavaScript that browsers and Node.js can execute.
What tsc does:
- ✅ Type checking
- ✅ Compiles TypeScript to JavaScript
- ✅ Generates your
/distoutput folder
What tsc doesn’t do:
- ❌ Run your code
- ❌ Bundle modules together
- ❌ Optimize or minify your code
The Modern Development Setup
The best practice today is to use both tools together:
tsxfor fast development and hot reloadingtscfor production builds and type checking
Here’s a typical package.json setup:
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"typecheck": "tsc --noEmit",
"start": "node dist/index.js"
}
}
Note: Using tsx for development alongside tsc for production is not only normal—it’s widely recommended. The slight differences between them are negligible in practice.
Mastering tsconfig.json
Your tsconfig.json file is where you configure how TypeScript compiles your code. Let’s dive into the settings that matter most.
Choosing Your Target: esnext vs es2022
The target option determines which JavaScript version your TypeScript compiles to.
esnextalways targets the absolute latest ECMAScript features that TypeScript supports, including experimental ones. It’s bleeding-edge but potentially unstable.es2022locks to the 2022 ECMAScript specification, providing consistent and predictable behavior.
For production code, use es2022 (or another specific version). When you set "target": "es2022", make sure to also set "module": "ES2022" for consistency.
Organizing Your File Structure
Two key settings control where TypeScript finds your source files and where it outputs compiled code:
{
"rootDir": "./src",
"outDir": "./dist"
}
This creates a clean project structure:
project/
├── src/ ← Your TypeScript source code (rootDir)
│ ├── index.ts
│ └── routes/
├── dist/ ← Compiled JavaScript output (outDir)
│ ├── index.js
│ └── routes/
├── tsconfig.json
└── package.json
The esModuleInterop Magic
If you’re working with CommonJS libraries (and you probably are), set "esModuleInterop": true. This setting allows you to use clean default imports from CJS modules:
// ❌ Without esModuleInterop:
import express from "express"; // Error! express has no default export
// ✅ You'd need to do:
import * as express from "express";
const app = express.default();
// ✅ With esModuleInterop:
import express from "express"; // Works! TypeScript adds compatibility
Much better, right?
Happy coding! 🚀