TypeScript Project Structure

How the five root tsconfigs are structured and how every concrete config extends them.

The repo has five tsconfig files at the root: one absolute base plus four purpose-named bases that extend it. Every concrete tsconfig under apps/* or packages/* extends exactly one of those four; test configs use a specific array-extends pattern, documented below.

The five root files

FileExtendsFor
tsconfig.jsonAbsolute base. Strict, ESNext, moduleResolution. Not extended directly by any concrete config.
tsconfig.lib.jsontsconfig.jsonPackages in ./packages/**. Composite, declaration, sourceMap, conventional outDir/rootDir.
tsconfig.app.jsontsconfig.jsonNext.js apps in ./apps/**. noEmit, jsx: preserve, DOM libs, Next plugin.
tsconfig.test.jsontsconfig.json*.test.* files everywhere. Vitest/happy-dom types, jsx: react-jsx, noEmit.
tsconfig.node.jsontsconfig.jsonNode-runtime TS: vite.config.ts, vitest.config.ts, scripts.

The inheritance invariant

Every concrete tsconfig either:

  1. extends exactly one of the four bases, OR
  2. (Test configs only) extends an array of [host project's main config, "../../tsconfig.test.json"].

No other extends chain is sanctioned.

Note

apps/docs is the one documented exception. It extends ../../tsconfig.app.json like every other Next.js app in the monorepo — the Docusaurus-era exception no longer applies. apps/docs/tsconfig.typedoc.json is a separate TypeDoc input config used only by the docs generation scripts; it is not part of the normal TypeScript build graph.

Cookbook

Adding a package

// packages/<name>/tsconfig.json
{
    "$schema": "https://json.schemastore.org/tsconfig",
    "extends": "../../tsconfig.lib.json",
    "include": ["./src/**/*.ts"]
}

Adding a JSX-emitting package

{
    "$schema": "https://json.schemastore.org/tsconfig",
    "extends": "../../tsconfig.lib.json",
    "compilerOptions": {
        "jsx": "preserve",
        "lib": ["ESNext", "DOM"]
    },
    "include": ["./src/**/*.ts", "./src/**/*.tsx"]
}

Adding a nested package (one level deeper)

Use one extra ../. The @tagtree/* packages live under packages/tagtree/*:

// packages/<group>/<name>/tsconfig.json
{
    "$schema": "https://json.schemastore.org/tsconfig",
    "extends": "../../../tsconfig.lib.json",
    "include": ["./src/**/*.ts"]
}

Adding a Next.js app

// apps/<name>/tsconfig.json
{
    "$schema": "https://json.schemastore.org/tsconfig",
    "extends": "../../tsconfig.app.json",
    "compilerOptions": {
        "paths": {
            "@/components/*": ["./src/components/*"]
        }
    },
    "include": [
        "next-env.d.ts",
        "./src/**/*.ts",
        "./src/**/*.tsx",
        ".next/types/**/*.ts",
        ".next/dev/types/**/*.ts"
    ]
}

Adding tests to a package or app

// <project>/tsconfig.test.json
{
    "$schema": "https://json.schemastore.org/tsconfig",
    "extends": ["./tsconfig.json", "../../tsconfig.test.json"],
    "include": ["./src/**/*.test.ts", "./src/**/*.test.tsx"]
}

The order matters: host config first (for paths/conventions), test base second so its types/jsx/noEmit win on conflict.

Adding a vite.config.ts or vitest.config.ts

// <project>/tsconfig.node.json
{
    "$schema": "https://json.schemastore.org/tsconfig",
    "extends": "../../tsconfig.node.json",
    "include": ["vite.config.ts", "vitest.config.ts"]
}

Where compiler options live

OptionLives in
strict, target, module, moduleResolution, libtsconfig.json
composite, declaration, declarationMap, sourceMap, incremental, outDir, rootDir, types: ["node"]tsconfig.lib.json
noEmit, jsx: preserve, jsxImportSource, DOM lib, Next plugins, relaxed noUnused*/noUncheckedIndexedAccesstsconfig.app.json
types: ["vitest/globals", "happy-dom"], jsx: react-jsx, test noEmit, test compositetsconfig.test.json
types: ["node"], ESNext-only lib, node composite, node noEmittsconfig.node.json

A note on app strictness

The app base relaxes a few checks that the absolute base enables:

  • noUnusedLocals: false and noUnusedParameters: false — framework callback signatures (NextAuth, Next route handlers) commonly have unused args. Libraries keep these checks on.
  • noUncheckedIndexedAccess: false — older app code uses indexed access without undefined-checks. The flag is on for libraries; apps were never strict here.

Anti-patterns (refuse in review)

  • "references": [{ "path": "../../tsconfig.json" }] without composite on both ends. Project references graph is Phase 2; do not paste this pattern.
  • "typeRoots": ["./dist/index.d.ts"]. typeRoots takes directories, not files. Pointing at your own dist is circular.
  • Apps re-declaring base strict options (strict, target, module, etc.) inline. Extend the base instead.
  • Extending another concrete config (e.g. a package's tsconfig.json extending another package's). The only sanctioned multi-extend is the array form in test configs.
  • Adding JSX or DOM lib in the lib base. Packages opt in individually.
Continue exploring

On this page