← All articles
LANGUAGES TypeScript Tooling: Compiler Options, Runtimes, and ... 2026-02-09 · 4 min read · typescript · tsconfig · bun

TypeScript Tooling: Compiler Options, Runtimes, and Monorepo Setup

Languages 2026-02-09 · 4 min read typescript tsconfig bun monorepo compiler

TypeScript Tooling: Compiler Options, Runtimes, and Monorepo Setup

TypeScript's value proposition is simple: catch bugs before they reach production. But the tooling around TypeScript has grown sprawling enough that configuring it well is a skill in itself. This guide covers the compiler options that actually matter, how to structure tsconfig.json for different project types, running TypeScript directly without a build step, and scaling TypeScript across a monorepo.

Compiler Options That Actually Matter

The tsconfig.json file has over 100 options. Most of them you can ignore. Here are the ones worth understanding.

Strictness

Turn on strict: true. It enables a bundle of checks (strictNullChecks, strictFunctionTypes, strictBindCallApply, noImplicitAny, etc.) and is the single most impactful setting. If you're starting a new project without strict: true, you're leaving TypeScript's best features on the table.

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

noUncheckedIndexedAccess is not included in strict but is worth enabling. It makes array and object index access return T | undefined instead of T, catching a common class of runtime errors.

Module Resolution

This is where most confusion lives. The short version:

Project Type module moduleResolution
Modern Node.js (ESM) NodeNext NodeNext
Bundled (Vite, webpack) ESNext Bundler
Library (dual CJS/ESM) NodeNext NodeNext
Bun ESNext Bundler

The Bundler module resolution was added in TypeScript 5.0 and reflects how bundlers actually resolve imports (allowing extensionless imports, package.json exports, etc.). If you use Vite, Next.js, or any bundler, use it.

Target and Lib

Set target to the lowest runtime you support. For server-side code targeting Node 20+, ES2022 or ESNext is fine. For browser code, let your bundler handle downleveling and set target: "ESNext".

lib should match your runtime. For Node.js: ["ES2023"]. For browsers: ["ES2023", "DOM", "DOM.Iterable"].

tsconfig.json for Different Project Types

Node.js API Server

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "dist",
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

Vite/React Frontend

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "jsx": "react-jsx",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "noEmit": true
  },
  "include": ["src"]
}

Notice noEmit: true for bundled projects. TypeScript only does type checking; the bundler handles the actual compilation.

Shared Library

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

declarationMap is important for libraries -- it allows "Go to Definition" to jump to the original .ts source rather than the .d.ts file.

Running TypeScript Directly

You don't always need a build step. Several tools can run .ts files directly.

Bun

Bun has native TypeScript support with zero configuration. It strips types at startup and runs the JavaScript. This is the fastest option.

bun run src/index.ts
bun test

Bun does not type-check at runtime. It only strips types. You still need tsc --noEmit for actual type checking.

tsx

tsx is a Node.js-based TypeScript runner that uses esbuild under the hood. It's the best option when you need Node.js compatibility but want to skip the build step.

npx tsx src/index.ts
npx tsx watch src/index.ts  # with file watching

ts-node

ts-node was the original TypeScript runner but is slower than tsx and has more configuration overhead. Unless you have a specific reason (like needing transformers or path aliases), prefer tsx or Bun.

Node.js Native (--experimental-strip-types)

Node.js 22+ added --experimental-strip-types, which strips TypeScript types natively without a third-party loader. In Node 23.6+, it works without the flag for .ts files. This is worth watching but still has rough edges around features like enums and decorators.

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

Type Checking in CI

Your CI pipeline should run type checking as a separate step from your build. This catches type errors even when your bundler would happily ignore them.

# GitHub Actions example
- name: Type check
  run: bunx tsc --noEmit

# Or for monorepos with project references
- name: Type check
  run: bunx tsc --build --noEmit

Run type checking early in your CI pipeline, ideally in parallel with linting and tests. It's fast enough (usually under 30 seconds for medium projects) that there's no reason to skip it.

A common mistake: relying on your editor's TypeScript integration as your only type checking. Editors can have stale caches, different TypeScript versions, or misconfigured settings. CI is the source of truth.

Monorepo TypeScript with Project References

When your monorepo has multiple packages that depend on each other, TypeScript project references let you type-check them efficiently.

Structure

packages/
  shared/
    tsconfig.json
    src/
  api/
    tsconfig.json    # references shared
    src/
  web/
    tsconfig.json    # references shared
    src/
tsconfig.json        # root, references all packages

Root tsconfig.json

{
  "files": [],
  "references": [
    { "path": "packages/shared" },
    { "path": "packages/api" },
    { "path": "packages/web" }
  ]
}

Package tsconfig.json (api)

{
  "compilerOptions": {
    "composite": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "references": [
    { "path": "../shared" }
  ],
  "include": ["src"]
}

The composite: true flag is required for any project that's referenced by another. It forces declaration: true and ensures TypeScript can do incremental builds.

Build the entire project graph with:

tsc --build

This compiles packages in dependency order and only rebuilds what changed. For large monorepos, this can cut type-checking time significantly compared to checking each package independently.

Trade-offs

Project references add configuration overhead. For small monorepos (2-3 packages), you might prefer a simpler setup where each package independently type-checks and relies on paths aliases. The break-even point is around 4-5 packages or when incremental build speed becomes noticeable.

Recommendations