← thecodex.expert · The Codex Family of Knowledge
TypeScript

tsconfig Deep Dive

target and lib control two genuinely different things — which JavaScript syntax gets compiled down, and which built-in APIs TypeScript assumes exist — and conflating them causes real, confusing bugs.

TypeScript 6.0 target ≠ lib Last verified:
Canonical Definition

target controls which version of JavaScript syntax the compiler emits (does a class compile to ES2015 class syntax, or older prototype-based code?), while lib controls which built-in APIs TypeScript assumes are available for type-checking (does Array.prototype.flat exist? does fetch?) — these are independent settings, and picking a lib newer than the actual runtime environment is a real, common source of "it type-checks but crashes at runtime" bugs. paths configures custom import path aliases (paired with baseUrl in older configs, but self-sufficient with relative entries since TS 4.1 — baseUrl itself is deprecated as of TS 6.0), and extends lets one tsconfig.json inherit settings from a shared base file, common in monorepos.

target vs lib: two genuinely different settings

Setting lib to a newer JavaScript version than the actual runtime supports means TypeScript will happily type-check code using an API that doesn't exist at runtime yet — a real gap between what compiles and what actually works.

TypeScripttsconfig.json
{
  "compilerOptions": {
    "target": "es2020",              // what SYNTAX gets emitted (arrow fns, classes, etc.)
    "lib": ["es2020", "dom"]          // what APIs TypeScript assumes EXIST (Array methods, fetch, etc.)
  }
}
// mismatch example: lib includes "es2023" (Array.prototype.toSorted)
// but the actual deployment target is an older Node.js — compiles fine, crashes at runtime

Path aliases with paths (and, historically, baseUrl)

Without this, deeply nested files accumulate long relative import chains (../../../components/Button); a path alias replaces that with a clean, absolute-feeling import that doesn't break when a file moves. Older configs pair paths with baseUrl, and you'll still see that combination everywhere, but it's no longer necessary: since TypeScript 4.1, paths entries written relative to the config file (with a leading ./) resolve on their own. baseUrl has since been marked deprecated as of TypeScript 6.0 — tsc now reports it as an error (TS5101) unless you opt out with "ignoreDeprecations": "6.0" — and it stops working altogether in TypeScript 7.0. New configs should skip baseUrl and write paths with explicit relative prefixes instead.

TypeScripttsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@components/*": ["./src/components/*"],
      "@utils/*": ["./src/utils/*"]
    }
  }
}
TypeScriptApp.tsx
import { Button } from "@components/Button";   // instead of "../../../components/Button"

Note that tsc only resolves these aliases for type-checking — a bundler (Vite, webpack) or Node's own resolution needs its own matching alias configuration to actually run the code.

Sharing config with extends

A monorepo commonly has one shared base tsconfig with company-wide settings, and each package extends it with only its own specific overrides — avoiding dozens of near-duplicate config files that could drift out of sync.

TypeScripttsconfig.base.json
{
  "compilerOptions": {
    "strict": true,
    "target": "es2020",
    "module": "esnext"
  }
}
TypeScriptpackages/app/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

Sources

1
Microsoft. TSConfig Reference, typescriptlang.org/tsconfig, covering target, lib, paths, baseUrl, and extends.