Right, let’s get this party started. You’ve got a sprawling JavaScript project, and you’ve decided to inject some sanity into it with TypeScript. The very first, and arguably most important, step is to drop a tsconfig.json file in the root of your project. This file is the mission control for the TypeScript compiler (tsc); it tells it exactly how to behave, what files to look at, and how strictly to judge your life choices. Think of it less as a configuration file and more as a rulebook you get to write for your new, more disciplined coding life.

The absolute fastest way to generate a baseline tsconfig.json is to let TypeScript itself do the heavy lifting. Open your terminal in the project root and run:

npx tsc --init

This will spit out a tsconfig.json file with a ton of options, most of them commented out. It’s a decent starting point, but it’s like being handed a helicopter cockpit manual when you just need to know how to start the engine. We need to curate this.

The Non-Negotiable Core Settings

Let’s cut through the noise. Here are the settings you should absolutely configure from the get-go. This is the “no, seriously, do this” list.

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "allowJs": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

Let’s break down the why:

  • "target": "ES2020": This tells tsc to compile your modern TypeScript down to ES2020 JavaScript. It’s a good balance between modern features and broad support. You can set it higher if you’re only targeting ever-green browsers.
  • "module": "ESNext" & "moduleResolution": "node": You’re likely using a bundler like Webpack or Vite. These settings tell TypeScript to output modern ES modules and to resolve them using the same Node.js algorithm your bundler uses. It prevents “cannot find module” headaches.
  • "allowJs": true: This is your migration lifeline. It allows the TypeScript compiler to process .js and .jsx files. You can’t migrate gradually if TypeScript refuses to even look at your old code.
  • "outDir": "./dist", "rootDir": "./src": These keep your compiled output separate from your source code. It’s just good hygiene. The compiler will mirror your src directory structure inside dist.
  • "strict": true: This flips on the entire strict type-checking family. This is the whole point, isn’t it? It will hurt at first—you’ll have a thousand errors—but it’s what forces you to write truly type-safe code. Embrace the pain; it’s productive pain.
  • "esModuleInterop": true: This fixes a bunch of historical baggage around importing CommonJS modules (like those from the npm registry) as if they were ES modules. You almost always want this on. The fact that it’s not the default is a testament to JavaScript’s weird history.
  • "skipLibCheck": true: This skips type checking of declaration files (.d.ts) from your node_modules. This can significantly speed up compilation because it avoids checking, say, all of @types/react every time. It’s generally safe.

Your Include/Exclude Strategy

The "include" array is crucial. It tells TypeScript which files to actually pick up and compile. If you just run tsc without an include, it will grab every .ts file it can find, including those in node_modules, which is a disaster. Be specific. The example above only looks in the src directory.

The "exclude" array is its often-ignored sibling. You don’t usually need it if your include is well-defined, but it’s useful for telling TypeScript to explicitly ignore things like your dist folder, tests, or scripts. You don’t want TypeScript wasting its energy on your build output.

The Gradual Migration Mindset

Here’s the most important part: your first tsconfig.json is a living document. You are not going to fix all the errors on day one. That’s okay. The goal is to start the process.

The nuclear option is to set "strict": true immediately and face the wall of red errors. This is the “rip the band-aid off” approach, and it works for smaller, braver teams.

The more pragmatic, and I’d argue smarter, approach is to temporarily dial down the strictness. You can start with "strict": false and then enable individual strict flags one by one as you fix errors. This lets you migrate in phases. Maybe you turn on "noImplicitAny" first and fix all those errors. Then a week later, you turn on "strictNullChecks". This is how you avoid paralyzing your team with 10,000 errors on a Monday morning.

The key is to agree that this is a temporary state. Your tsconfig.json should get stricter over time, not stay lax. The entire point is to get to "strict": true. Use this file to control the pace of your march toward that goal.