TypeScript configuration
A monorepo can grow a tsconfig per package, a base file, references and path aliases, and each one is a place for settings to drift. This page answers how many programs there are, which flags are on, and which compiler binary actually runs the Effect diagnostics.
Keep one root tsconfig.json and no other
Section titled “Keep one root tsconfig.json and no other”Impact: HIGH one file cannot drift from itself
- The root
tsconfig.jsonis the only tsconfig. Every.tsfile underapps/,packages/andscripts/, plus root files likevitest.config.tsandalchemy.run.ts, is in one program under one set of flags. - No
referencesand nocomposite: they exist to emit, and we emit nothing. Packages export source. - No
paths. A workspace import resolves through pnpm’s symlink and the package’sexportsto the source file, as Node does at runtime. Apathsalias would resolve a package the importer never declared (see monorepo tooling). - Accepted cost: one
typesand onelibfor every file, and no per-package incremental check.
❌ Incorrect — a base file, per-package configs, and an alias that bypasses the graph:
{ "extends": "../../tsconfig.base.json", "compilerOptions": { "composite": true, "paths": { "@app/contracts": ["../contracts/src"] } }, "references": [{ "path": "../contracts" }]}✅ Correct — one program at the root:
// tsconfig.json (root) — the only tsconfig in the repo{ "$schema": "./node_modules/@effect/tsgo/schema.json", "include": ["*.ts", "apps/**/*.ts", "packages/**/*.ts", "scripts/**/*.ts"], "compilerOptions": { /* … */ }}Source: notes/11-repo-operations/typescript-config.md · Decision 1
Turn on strict plus four flags
Section titled “Turn on strict plus four flags”Impact: HIGH optionalKey is only a distinction with exactOptionalPropertyTypes
exactOptionalPropertyTypesmakes “absent, neverundefined” real (see schema modeling).noUncheckedIndexedAccesstypesxs[i]asT | undefined, which is what it is at runtime.noImplicitOverridebecause services and tagged errors are classes;noFallthroughCasesInSwitchbecause a fall-through is almost always a missingreturn.noErrorTruncationbecause Effect types are long and a truncated error hides theE/Rmismatch.- Not set:
noUnusedLocals/noUnusedParameters(oxlint already failscheckon them),isolatedModules,noPropertyAccessFromIndexSignature.
❌ Incorrect — two gates for unused variables, and Effect errors cut off:
"strict": true,"noUnusedLocals": true,"noUnusedParameters": true✅ Correct — the strictness block:
"strict": true,"exactOptionalPropertyTypes": true,"noUncheckedIndexedAccess": true,"noImplicitOverride": true,"noFallthroughCasesInSwitch": true,"noErrorTruncation": true,"skipLibCheck": trueSource: notes/11-repo-operations/typescript-config.md · Decision 2
Resolve like Node: nodenext, .ts specifiers, erasable syntax
Section titled “Resolve like Node: nodenext, .ts specifiers, erasable syntax”Impact: HIGH tsc rejects what Node would reject at startup
module: nodenextmodels what runs the code: Node runs our.tsdirectly by stripping types. It also makes a missing import extension a compiler error (TS2835), whichbundleraccepts.allowImportingTsExtensions+noEmit.verbatimModuleSyntax+erasableSyntaxOnly: type-only imports sayimport type, and there are noenums,namespaces or parameter properties.- Every
package.jsondeclares"type": "module". tsc does not check it, so a manifest that drops it is a review comment. - A dependency that fails to resolve under
nodenextis fixed at that dependency (atypescondition or a localdeclare module), never by switching tobundler.
❌ Incorrect — bundler resolution and syntax Node cannot strip:
// "moduleResolution": "bundler"import { Rule } from "./rule" // no extension, acceptedexport enum Status { Active, Paused } // not erasable✅ Correct — what Node runs is what tsc checks:
// "module": "nodenext"import type { Rule } from "./rule.ts"export const Status = Schema.Literals(["active", "paused"])Source: notes/11-repo-operations/typescript-config.md · Decision 3
Run tsc --noEmit from typescript@7, patched by effect-tsgo
Section titled “Run tsc --noEmit from typescript@7, patched by effect-tsgo”Impact: HIGH the wrong binary drops every Effect diagnostic with exit 0
- The compiler is the
typescriptpackage at 7.x, binarytsc, patched byeffect-tsgo patchinprepare. There is no@typescript/native-previewand notsgobinary. tsgo --noEmitruns the wrong binary: it either does not exist, or it is an unpatched preview that silently skips the language-service diagnostics set toerror(see linting and formatting).tsc --noEmitruns once from the root, insidecheck.typescriptand@effect/tsgoare catalog entries bumped together in one commit, to a version that@effect/tsgolists as supported.- Check the gate is armed: the version prints the patch, e.g.
7.0.2+effect-tsgo.0.47.2.
❌ Incorrect — the unpatched preview binary:
"devDependencies": { "@typescript/native-preview": "…" },"scripts": { "check": "… && tsgo --noEmit" }✅ Correct — the patched tsc:
"scripts": { "prepare": "effect-tsgo patch", "check": "node scripts/workspace-graph.ts && node scripts/circular.ts && oxfmt --check && oxlint && tsc --noEmit"},"devDependencies": { "typescript": "catalog:", "@effect/tsgo": "catalog:" }Source: notes/11-repo-operations/typescript-config.md · Decision 4
Write types for the runtime profile; never add DOM
Section titled “Write types for the runtime profile; never add DOM”Impact: MEDIUM without a provider, AbortSignal and URL become a silent any
typesis always written: TypeScript 7 loads no@types/*on its own. It is the one line that differs by profile (see runtime and entrypoint).libis["esnext"]everywhere. The web globals Effect’s declarations name come from@types/node, bun-types or workers-types, never the browser lib.- The
@types/nodemajor matches.node-version. On the Worker profile workers-types are ambient, since bindings come from alchemy. - Accepted cost: on the Worker profile every file sees Worker globals, including Node scripts.
| profile | "types" |
|---|---|
| Node (default) | ["node"] |
| Bun | ["bun"] |
| Cloudflare Worker | ["node", "@cloudflare/workers-types"] |
❌ Incorrect — no types, and the browser lib for web globals:
"lib": ["esnext", "DOM"]✅ Correct — the profile’s types, no DOM:
"lib": ["esnext"],"types": ["node"]Source: notes/11-repo-operations/typescript-config.md · Decision 5
Deferred
Section titled “Deferred”- Incremental checking, first
"incremental": truewith atsBuildInfoFileundernode_modules/.cache/, project references only after that — trigger: the roottsc --noEmittakes more than 60 seconds locally. - A second program (another
tsconfig.jsonwith its owntypes/liband a secondtsc -pincheck) — trigger: the first workspace member that runs outside the project’s runtime profile, such as a browser app that needsDOMand JSX.