On this page

Setup and tsconfig.json

12 min read TextCh. 1 — TypeScript Fundamentals

Setting up TypeScript

TypeScript can be installed globally or locally per project. For any serious project, install it locally — this ensures every contributor uses the same version and your CI environment is reproducible.

# Create a new project directory
mkdir my-ts-project && cd my-ts-project

# Initialize npm
npm init -y

# Install TypeScript as a dev dependency
npm install -D typescript

# Verify the installed version
npx tsc --version   # Version 6.0.x

Once installed, create a source file to verify everything works:

// src/index.ts
const greeting: string = "Hello, TypeScript 6.0!";
console.log(greeting);

Compile and run it:

npx tsc src/index.ts --target ES2022
node src/index.js

The tsconfig.json file

For any project with more than one file, you need a tsconfig.json. This file tells the compiler where to find source files, where to put the output, and which rules to enforce. Generate a starting point with:

npx tsc --init

This creates a tsconfig.json with every option commented out. The example in the code panel shows a production-ready configuration for a Node.js or full-stack project. Let us walk through each important option.

Target and module

target controls the JavaScript version that tsc outputs. Modern projects should use ES2022 or later, which preserves native async/await, optional chaining, and class fields without polyfilling them.

module controls how import/export statements are compiled. The correct value depends on where your code runs:

Environment Recommended module
Node.js 18+ NodeNext
Browser (bundled by Vite/esbuild) ESNext
Browser (no bundler) ES2022
Legacy CommonJS Node.js CommonJS

moduleResolution must match module. When module is NodeNext, set moduleResolution to NodeNext as well. This enables Node.js-style resolution with support for the exports field in package.json, subpath imports, and .js extensions in TypeScript imports.

// With NodeNext resolution, imports must use the .js extension
// (TypeScript resolves .ts files but the compiled output uses .js)
import { formatDate } from "./utils/date.js";

The strict flag

strict: true is the single most important option in any tsconfig.json. It is a shorthand that enables eight individual strict checks simultaneously. The most impactful one is strictNullChecks, which makes null and undefined distinct types rather than assignable to everything:

// Without strictNullChecks — dangerous
function getLength(text: string): number {
  return text.length; // what if text is null?
}

// With strictNullChecks — safe
function getLength(text: string | null): number {
  if (text === null) return 0;
  return text.length; // TypeScript knows text is string here
}

Without strictNullChecks, TypeScript misses an entire category of null pointer errors. Always enable strict.

outDir and rootDir

These two options keep your project organized:

  • rootDir — the root folder of your source files (usually ./src). TypeScript will mirror this folder structure in the output.
  • outDir — where compiled JavaScript files are written (usually ./dist).

With rootDir: "./src" and outDir: "./dist", the file src/app/server.ts compiles to dist/app/server.js.

esModuleInterop

This option generates helper code that allows you to import CommonJS modules (like those from npm) using default import syntax:

// Without esModuleInterop — verbose
import * as path from "path";

// With esModuleInterop — natural
import path from "path";

Always enable this for Node.js projects. Most modern TypeScript starter templates include it.

paths — module aliases

The paths option maps import aliases to real file locations, eliminating long relative imports deep in nested folders:

// Without paths — fragile and hard to read
import { UserService } from "../../../services/user.service";

// With paths configured — clean and refactor-safe
import { UserService } from "@app/services/user.service";

The tsconfig.json configuration for this is shown in the code example. Remember that paths only affects the TypeScript compiler — you need matching alias configuration in your bundler or Node.js loader.

declaration and source maps

For libraries published to npm, enable declaration: true to generate .d.ts type definition files alongside the compiled JavaScript. Consumers of your library get full autocomplete without needing access to your source.

declarationMap: true generates .d.ts.map files that allow IDE "go to definition" to jump to the original TypeScript source instead of the generated declaration file.

sourceMap: true generates .js.map files that map compiled JavaScript lines back to the original TypeScript source. This makes stack traces in Node.js and browser DevTools readable.

Running TypeScript in Node.js

For development scripts and backend applications you often want to run TypeScript files directly without a separate compile step.

tsx is the recommended tool:

npm install -D tsx

# Run a TypeScript file directly
npx tsx src/server.ts

# Watch mode — restart on file changes
npx tsx watch src/server.ts

tsx uses esbuild internally and is extremely fast. It skips type checking (use tsc --noEmit separately for that) and focuses purely on execution speed.

ts-node is the older alternative. It is slower but supports more configuration options and is sometimes required by tools that rely on the Node.js require hook:

npm install -D ts-node @types/node

# Run with ts-node
npx ts-node src/server.ts

# Run with the faster SWC transpiler
npx ts-node --swc src/server.ts

Project references for monorepos

Large monorepos with multiple TypeScript packages benefit from project references, which allow incremental compilation across packages:

// packages/api/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist"
  },
  "references": [
    { "path": "../shared" }
  ]
}

With composite: true, TypeScript builds only the packages that changed since the last build. Running tsc --build (or tsc -b) resolves the dependency graph and compiles packages in the correct order.

A minimal but production-ready setup

For a new project, here is a minimal workflow that works immediately:

npm init -y
npm install -D typescript tsx @types/node

# Generate tsconfig.json, then configure it
npx tsc --init

Edit the generated tsconfig.json to match the example in the code panel, then add these scripts to package.json:

{
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "typecheck": "tsc --noEmit",
    "start": "node dist/index.js"
  }
}

This gives you fast iteration during development (dev), strict type checking on demand (typecheck), and a clean compiled output for production (build + start).

Practice

  1. Create a new project, install TypeScript, and run npx tsc --init. Compare the generated file to the example in this lesson. Identify three options that are disabled by default but should be enabled.
  2. Write a small TypeScript file that imports a built-in Node.js module (e.g., path or fs). Try compiling it both with and without esModuleInterop. Observe the difference in the compiler output.
  3. Add a paths alias to your tsconfig.json, create a file at the aliased location, and import it using the alias. Verify that the compiler resolves it correctly.

Always enable strict mode
The `strict` flag enables eight individual checks: strictNullChecks, strictFunctionTypes, strictPropertyInitialization, noImplicitAny, noImplicitThis, alwaysStrict, strictBindCallApply, and useUnknownInCatchVariables. Disabling it is the single most common source of TypeScript bugs in the wild. Never start a project without it.
Use tsx for fast Node.js execution
tsx (npm install -D tsx) runs TypeScript files directly with Node.js using esbuild under the hood. It is much faster than ts-node for development scripts: `npx tsx src/script.ts`. For production, always compile with tsc first.
paths vs bundler resolution
The `paths` option in tsconfig.json only affects the TypeScript compiler. Your bundler (Vite, Webpack, esbuild) or runtime (Node.js) needs separate alias configuration to resolve the same paths at runtime.
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "paths": {
      "@app/*": ["./src/app/*"],
      "@utils/*": ["./src/utils/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.spec.ts"]
}