tsconfig

star 4.6k

TypeScript config conventions for this monorepo: the two-base layering, the eight leaf tiers, and the never-redeclare list. Use when adding a package, editing any tsconfig.json, picking a tier for a new app, or debugging module resolution.

EpicenterHQ By EpicenterHQ schedule Updated 6/8/2026

name: tsconfig description: 'TypeScript config conventions for this monorepo: the two-base layering, the eight leaf tiers, and the never-redeclare list. Use when adding a package, editing any tsconfig.json, picking a tier for a new app, or debugging module resolution.' metadata: author: epicenter version: '1.0'

tsconfig conventions

Every package here is source-only .ts: exports point at ./src/*.ts, there is no build step, and consumers (Bun, Vite, WXT, Tauri, the Cloudflare Worker) operate on raw .ts. That single fact decides the whole config.

The one rule

A leaf tsconfig.json may set only: types, library-only strictness (noUnusedLocals/noUnusedParameters), checkJs (SvelteKit), and genuinely package-specific options (jsx, paths, customConditions, include).

Anything else belongs in a base. If a leaf option repeats a base value or a TypeScript default, delete it.

Two base files

tsconfig.base.json   universal: target/lib[ESNext], module preserve, strict,
                     noEmit, isolatedModules, verbatimModuleSyntax, types []
tsconfig.dom.json    extends base; its ONLY job is lib [ESNext, DOM, DOM.Iterable]

There is no tsconfig.base.lib.json and no project references. Source-only packages never emit, so composite/declaration/outDir have no place here.

Module strategy: one, repo-wide

module: "preserve" + moduleResolution: "bundler". preserve implies bundler (TS 5.4+); the explicit pair is kept for legibility. Never use NodeNext anywhere: nothing in this repo is published as emitted Node ESM, and bundler resolution already reads package.json imports/exports.

The eight leaf tiers

Pick the tier, copy the shape, change nothing else.

Tier extends Leaf adds
Bun library "../../tsconfig.base.json" types:["bun"], noUnusedLocals, noUnusedParameters
Node library "../../tsconfig.base.json" types:["node"], noUnusedLocals, noUnusedParameters
Svelte library "../../tsconfig.dom.json" types:["bun"], noUnusedLocals, noUnusedParameters
SvelteKit app ["../../tsconfig.base.json", "./.svelte-kit/tsconfig.json"] checkJs:true, types:["bun"]
Cloudflare Worker "../../tsconfig.base.json" jsx, jsxImportSource, types, include
Bun app "../../tsconfig.base.json" types:["bun"], include
Astro app ["../../tsconfig.base.json", "astro/tsconfigs/strict"] astro include/exclude
WXT extension ["../../tsconfig.dom.json", "./.wxt/tsconfig.json"] customConditions, paths, include

A generated config (./.svelte-kit/tsconfig.json, ./.wxt/tsconfig.json) goes last in the array so its lib/module win where they must. Never hand-edit a generated config.

The two canonical library shapes in full:

// bun library: packages/workspace, filesystem, util, sync, cli, ...
{
    "extends": "../../tsconfig.base.json",
    "compilerOptions": {
        "types": ["bun"],
        "noUnusedLocals": true,
        "noUnusedParameters": true
    }
}

// svelte library: packages/ui, svelte-utils
{
    "extends": "../../tsconfig.dom.json",
    "compilerOptions": {
        "types": ["bun"],
        "noUnusedLocals": true,
        "noUnusedParameters": true
    }
}

Never redeclare these (base value or TS default)

Putting any of these in a leaf is dead weight. Delete on sight.

Do not write in a leaf Why
module: "preserve" already in tsconfig.base.json
moduleResolution, target, noEmit, strict, isolatedModules already in the base
lib: ["ESNext"] the base default; use tsconfig.dom.json if you need DOM
noPropertyAccessFromIndexSignature: false that is already the TS default
useDefineForClassFields: true default when target >= ES2022
forceConsistentCasingInFileNames default true since TS 5.0
resolvePackageJsonExports default true under moduleResolution: bundler
sourceMap no-op under noEmit
single-element extends array use the string form: "extends": "../../tsconfig.base.json"

types is opt-in, on purpose

The base sets types: [] to disable auto-inclusion of every node_modules/@types package. Each leaf opts in: ["bun"] for almost everything, ["node"] for the one package that uses @types/node. Do not hoist ["bun"] to a base: a package without @types/bun installed would then fail with Cannot find type definition.

include rules

  • TypeScript does not merge include across extends. A leaf include fully replaces the inherited one. For SvelteKit apps this means: do not set your own include, or you drop the generated ambient.d.ts/$types globs.
  • Only set include when you must narrow scope for a real reason; comment why.

Adding a new package

  1. Scaffold via the monorepo skill's boilerplate.
  2. Pick a tier from the table above; copy that exact shape.
  3. package.json uses exports only, no main/types. Entry point is ./src/index.ts.
  4. bun install at the repo root, then bun typecheck.

Background

The full rationale, the migration that established this layout, and the baseline typecheck state are in specs/20260522T190000-modernize-monorepo-tsconfig.md.

Install via CLI
npx skills add https://github.com/EpicenterHQ/epicenter --skill tsconfig
Repository Details
star Stars 4,632
call_split Forks 351
navigation Branch main
article Path SKILL.md
More from Creator