tsconfig

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/epicenter5 installsMITSynced Aug 26

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
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.
license: MIT
---

# 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:

```jsonc
// 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`.

More Debugging skills

← All Debugging skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY