Agent-Primary TS + npm Starter (tsconfig · Prettier · Oxlint)
Use this when creating or configuring a TypeScript npm project, or when any of its three surfaces — tsconfig.json, Prettier, Oxlint — is touched individually. In a monorepo, edit the workspace the user names — ask if it isn't named.
Design Premise
The reader, editor, and primary consumer of this project is an LLM agent, not a human scanning top-to-bottom. Every default below moves the project from human-oriented readability to machine verifiability and machine explorability.
- Every mistake a flag can catch, a flag should catch. Agents emit plausible-wrong code faster than anyone reviews it; the errors that type checks, tests, and lints raise unasked are the only feedback loop that scales. Structure the project so wrong code snags — on
tsc, on the linter, on the test type-check — instead of sliding through to review. - The program must be legible file-by-file, without whole-program inference. Inferred export signatures, ambient globals, and
tsc-only path aliases are invisible to a grep, therefore invisible to the agent. Force them written down. - Tool output is a machine-consumed artifact. Truncated types, ANSI escapes, and version-floating defaults are corruption in that channel — hence
tsc --pretty false+noErrorTruncation, andoxlint --format agent. - Edits must not disturb what they don't touch. Formatter defaults tuned for human eyes reflow whole regions around a one-line agent edit, shifting line numbers and invalidating the
old_stringmatches the agent holds for its next edits. Formatting exists to keep diffs minimal and line numbers stable. - Prefer a checked element over a comment. Inline and doc comments are not continuously maintained in AI-driven development — they rot silently. The same invariant as a type,
satisfies,assertNever, or@ts-expect-errorfails the build when it stops being true. See Comments → Checked Artifacts. - A green
tscis not a working program.tscchecks types, not module resolution at runtime. Config choices that are legal to the compiler and fatal to Node are this skill's most dangerous failure mode, so every emitting route ends in a runtime probe — see Compile-Success Is Not Runtime-Success.
This does not justify shrinking things for readability — no splitting files to keep them short, no avoiding long unions or deep generics, no simplifying types so a human can follow them linearly. Those optimize the false premise that a human reads top-to-bottom, at the cost of machine-verifiability.
Applying to a Project
For a full bootstrap, apply in this order:
- tsconfig — answer the six questions, pick the trio row, copy the strict core, then read
references/tsconfig.mdfor the use-case template and gate configs. Read that file before creating or editing any tsconfig file. - Prettier — section below.
- Oxlint — section below.
- Scripts — the unified block.
- Verification — checks below, including the runtime probe from the reference.
For a single-surface task (just the linter, just a tsconfig question), jump straight to that section; the standing policy still applies.
Before Any tsconfig Decision
Six answers decide the route. Ask for whatever isn't already evident; do not guess.
- Who emits JS —
tsc, a bundler, or a runtime that strips types? - Who resolves the output — Node, a browser via bundler, or another package's consumers? Independent of (1), and the usual source of broken configs.
- Lowest supported runtime — the actual Node/browser floor, which fixes
targetandlib. - JSX framework, if any.
- Test runner, and where test files live.
- Tools importing the TypeScript API — typescript-eslint, ts-morph, Volar-based framework tooling. See No Stable Compiler API in 7.0.
Environments without a route in the reference — Deno, Workers, Bun-only, browser-without-bundler, mixed ESM/CJS packages — get the standing policy (the strict flags), not a template. Their target/module/moduleResolution/lib come from that runtime's own documentation.
The target / module / moduleResolution Trio
Co-dependent. Pick one row, don't improvise. target/lib are floors to raise deliberately, not defaults.
| Scenario | target |
module |
moduleResolution |
Template |
|---|---|---|---|---|
| Bundled app | es2024 |
preserve |
bundler |
§1 |
| Node.js native ESM | es2024 |
nodenext |
nodenext |
§2 |
| Library emitted by tsc | es2022 |
nodenext |
nodenext |
§3 |
| Library emitted by a bundler | es2022 |
preserve |
bundler |
§3 |
| CommonJS authored source | es2022 |
commonjs |
bundler |
§6 |
- Never use
bundlerfor output thattscemits and Node loads.bundlerpermits extensionless relative imports andmodule: esnext/preservekeeps them verbatim, soexport { x } from "./utils"compiles clean and dies atnode dist/index.jswithERR_MODULE_NOT_FOUND.bundleris correct only when a bundler resolves the result. This is the single most expensive mistake in this skill's subject area. commonjs+bundleris a valid combination as of 6.0 and the recommended landing spot off the removedmoduleResolution: node— but see §6 for the strict-core exceptions it forces.module: "preserve"beats"esnext"for bundled apps: each import/export keeps its written form instead of being coerced.nodenextrequires an extension on relative imports (TS2835). Either write.jsyourself, or write.ts/.tsxand setrewriteRelativeImportExtensions: true, which rewrites those suffixes on emit. It does not add extensions to extensionless imports; there is no route that lets you omit them.
The Strict Core
Identical in every route — copy it verbatim, then add the use-case block from the reference. §6 CommonJS is the one documented exception.
{
"compilerOptions": {
"skipLibCheck": true,
"incremental": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"erasableSyntaxOnly": true,
"useDefineForClassFields": true,
"resolveJsonModule": true,
"allowJs": false,
"strict": true,
"noUncheckedIndexedAccess": true,
"noPropertyAccessFromIndexSignature": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"noUncheckedSideEffectImports": true,
"noErrorTruncation": true
}
}
Per-flag rationale for the strictness flags — what each catches and what it costs — is in Strict Flags Beyond strict: true; skipLibCheck and incremental are loop-vs-gate concerns covered in Two-Tier Checking.
Standing Policy (override only with a stated reason)
- Correctness —
strict: trueplus every flag in Strict Flags Beyondstrict: true. - Legibility without inference —
isolatedDeclarations(per route),moduleDetection: "force", explicittypes: [...](never["*"]),verbatimModuleSyntax+isolatedModules,erasableSyntaxOnly. - Output as a machine channel —
noErrorTruncation: true, and--pretty falseplus a pinned--checkerson every agent-facing script, not just CI. - Loop speed —
skipLibCheckandincrementalon in the inner loop, off at the gate — except undercomposite, which forbids it. See Two-Tier Checking.
Two documented exceptions, both in §6 CommonJS: verbatimModuleSyntax and erasableSyntaxOnly cannot both be on in a CJS-authored project. When the user proposes any other loosening, ask what concrete problem they are solving before agreeing.
Everything else about tsconfig — the TypeScript 7 baseline and its hard errors, per-route templates §1–§6, two-tier gate configs, monorepo project references, CommonJS, path aliases, determinism, migrating an existing tsconfig, and troubleshooting — is in references/tsconfig.md. Read it before writing any tsconfig file.
Prettier
Prettier's defaults are tuned for humans — its own docs recommend against printWidth over 80 "for readability". For an agent-primary project that premise inverts: wide reformat ripples around a one-line edit shift line numbers and silently invalidate the old_string matches and line references the agent is holding. These settings shrink that failure surface.
Install: npm add -D prettier. When prettier is already declared, keep its current version unless the user asks for a change.
Create .prettierrc.json if it does not exist:
{
"printWidth": 120,
"trailingComma": "all",
"arrowParens": "always",
"semi": true,
"endOfLine": "lf"
}
Why each value:
printWidth: 120— the one deliberate deviation from the default (80). Wider width means a one-line agent edit rarely triggers auto-wrap of surrounding lines, so line counts stay stable across edits.trailingComma: "all"— appending an item to an array, object, or argument list is a single added-line diff instead of a two-line diff that also rewrites the previous line's,.arrowParens: "always"— adding a type annotation to an arrow parameter doesn't force inserting parens, so the annotation edit stays a localized change.semi: true— ASI quirks don't change behavior when an agent prepends a line starting with[,(,+,-, or/.endOfLine: "lf"— no whole-file CRLF reformat noise when Windows and Unix contributors share the repo.
trailingComma, arrowParens, semi, and endOfLine restate Prettier 3's current defaults — deliberately. Defaults move across majors (trailingComma flipped es5→all in 3.0, endOfLine flipped auto→lf in 2.0), and a pinned value keeps a prettier bump from silently reformatting the whole repo. Same doctrine as pinning target/lib in tsconfig.
Create .prettierignore if it does not exist (gitignore syntax; VCS directories and node_modules are ignored by default, and Prettier also follows .gitignore from the directory it runs in — the explicit list below survives repos where those assumptions fail):
node_modules
dist
build
coverage
*.min.js
package-lock.json
*.md
*.md is listed because Prettier's markdown formatter reflows paragraphs, list spacing, and table column widths across edits, which breaks old_string matches the same way reformatted TS would — and markdown is content, not code, so formatting consistency isn't worth the agent-edit cost.
When .prettierrc.* already exists, keep it and surface the diff against the values above; when .prettierignore exists, append only missing entries.
Oxlint
The linter's consumer is the agent, so error-level findings are real bugs, style is the formatter's problem, and output is machine-parseable. Oxlint ships a dedicated agent value for --format — an output format the oxc team designed explicitly for LLM agent consumption — which the scripts below wire into the default lint command.
Install: npm add -D oxlint. When oxlint is already declared, keep its current version unless the user asks for a change.
Create .oxlintrc.json if it does not exist:
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "unicorn", "oxc", "import", "promise", "node"],
"categories": {
"correctness": "error",
"suspicious": "warn",
"perf": "warn",
"style": "off",
"pedantic": "off"
},
"rules": {
"no-console": "off"
},
"ignorePatterns": ["node_modules", "dist", "build", "coverage", "*.min.js"]
}
Why each value (verified against oxlint 1.80.0):
$schema— the config itself becomes machine-verified: a typo'd key or wrong value type is flagged by any JSON-schema-aware editor or validator instead of being silently ignored.plugins— settingpluginsoverwrites the default plugin set (typescript,unicorn,oxc; ESLint core rules always stay on) rather than extending it, so the array restates the defaults, then addsimport,promise,nodefor the TS/Node project surface. A list that omitsunicornoroxcsilently drops their correctness rules — and withstyle/pedanticoff, keeping them costs no noise, because categories gate severity across all plugins.categories.correctness: "error"— code that is definitely wrong blocks, so the agent fixes it before moving on.suspicious/perf: "warn"— informative without halting; the agent decides.style/pedantic: "off"— stylistic warnings in an agent edit loop drown out real bugs; style is Prettier's domain.rules.no-console: "off"— agent debugging routinely insertsconsole.log; flagging it slows the inner loop. Strip at release time with a separate gate, not on every lint.
This config is the untyped inner loop. At the gate, oxlint --type-aware --type-check (with oxlint-tsgolint installed) adds typescript/no-floating-promises and the no-unsafe-* family, which untyped lint and tsc both miss — see The Gate for the setup and its constraints.
When .oxlintrc.json already exists, keep it. Surface the diff against the values above so the user can decide whether to migrate, but do not overwrite.
npm Scripts
One block wires all three surfaces. Add to package.json (single-project; monorepos use the build-mode scripts in §4 instead):
{
"scripts": {
"typecheck": "tsc --noEmit --pretty false --checkers 4",
"typecheck:watch": "tsc --noEmit --watch --pretty false --checkers 4",
"typecheck:test": "tsc -p tsconfig.test.json --pretty false --checkers 4",
"typecheck:test:ci": "tsc -p tsconfig.test.json --skipLibCheck false --incremental false --pretty false --checkers 4",
"typecheck:ci": "tsc -p tsconfig.ci.json --pretty false --checkers 4",
"check:decl": "tsc -p tsconfig.declarations.json --pretty false --checkers 4",
"lint": "oxlint --format agent",
"lint:fix": "oxlint --fix",
"lint:strict": "oxlint --deny-warnings",
"format": "prettier --write .",
"format:check": "prettier --check ."
}
}
- The same
--pretty falseand--checkers Non everytscscript: an agent reads all of them, so a script that keeps ANSI output or floats its checker count is an inconsistency the agent has to absorb. --noEmitis only meaningful intypecheckfor a route whose base doesn't already set it; for §2/§3 it overrides an emitting config, which is what you want in the loop.check:declexists only wheretsconfig.declarations.jsondoes (§1/§2); pointing it at a §3 project is a TS5058 missing-file error — there the emitting build is its own declaration gate, so drop the script.lint:fixapplies auto-fixable rule corrections in one pass — oxlint's--fixis the safe tier that doesn't change behavior.lint:strictpromotes warnings to a non-zero exit code for CI gating.- Collision policy: when
typecheck,lint, orformatalready targets a different tool, leave it untouched and addtypecheck:tsc,lint:oxlint, orformat:prettieralongside.
Existing Projects
Do not overwrite an existing tsconfig.json, .prettierrc.*, or .oxlintrc.json. Surface the diff against the matching template, flag standing-policy violations, and let the user decide. For tsconfig, follow the stepwise Merge Rules — several strict flags produce hundreds of errors on first enable and are sequenced deliberately.
Comments → Checked Artifacts
Inline and doc comments are not continuously maintained in AI-driven development — they rot silently while the code moves. When you find the left column, replace it with the right column: the same information, expressed in an element the toolchain maintains for you.
| Rotting comment | Checked replacement |
|---|---|
// @ts-ignore |
// @ts-expect-error <reason> — errors once fixed, so it self-deletes |
// this is a user id, not a name |
branded type: string & { readonly brand: unique symbol } |
// one of: 'a' | 'b' | 'c' |
literal union, or as const object + (typeof X)[keyof typeof X] |
// handle new variants here too |
default: return assertNever(x) |
// keep in sync with X |
derive from the one source of truth: keyof, typeof, Extract, mapped types |
// keep in sync with the config |
satisfies Config on the literal |
// this cast is safe because … |
a validator that actually checks, plus tests — a hand-written x is T predicate can still lie, so prefer an inferred predicate or a runtime check over asserting one |
// returns null if not found |
put it in the return type: T | null |
// this function's shape is … |
isolatedDeclarations makes the signature mandatory |
// subtle overload, don't break it |
type-level test: expectTypeOf (vitest) or tsd in *.test-d.ts |
Keep two comment forms, because they are machine-consumed: @deprecated and @ts-expect-error. For sweeping the remaining unmaintained comments out of a codebase wholesale, the companion ts-comment-purge skill does it mechanically.
Verification
npx tsc --showConfig -p <each config>matches the table in Two-Tier Checking — check resolved values per file, not the snippets.npm run typecheck,typecheck:test,typecheck:test:ci,typecheck:ci(with no.tsbuildinfopresent), and — §1/§2 —check:declall exit 0.npm run lintexits 0 on a clean repository;.oxlintrc.jsonand.prettierrc.jsonparse as valid JSON.npm run formatruns without error across the repository;npm run format:checkexits 0 after a one-timenpm run format.- The emitted program actually runs — the runtime probe for the chosen route in Compile-Success Is Not Runtime-Success. A green
tscplus a failednode dist/index.jsis a resolver-level config gap, not a code bug — diagnose against that section's cause list. - A deliberate
const x: string = arr[0]errors — confirmsnoUncheckedIndexedAccessis live. - An error containing a long type shows it in full, not
... N more ...— confirmsnoErrorTruncation.
Route-specific checks (monorepo gate traversal, declaration maps, tarball probes) are in the reference's Verification.
References
- tsconfig deep reference (this skill):
references/tsconfig.md - TypeScript compiler options: https://www.typescriptlang.org/tsconfig
- Prettier options: https://prettier.io/docs/en/options
- Oxlint config: https://oxc.rs/docs/guide/usage/linter/config
- Oxlint CLI: https://oxc.rs/docs/guide/usage/linter/cli
- Oxlint type-aware linting: https://oxc.rs/docs/guide/usage/linter/type-aware