Files
atelier/languages/ts-tooling.md
T
Jon Chery 4e433158cd docs(P03): complete language-derived extension — v0.4
---ci---
project: atelier
phase: 3
milestone: v0.4
status: complete
phase_role: execution
phase_tag: v0.3.3
requirements:
  covered: [ATELIER-102, ATELIER-103, ATELIER-104, ATELIER-105]
  partial: []
---/ci---
2026-08-05 16:07:21 +00:00

5.6 KiB

TypeScript Tooling — Derived Application

Applies Atelier's domain principles to TypeScript tooling specifically. Derives from domains/ docs; introduces no new P-rules (D-063). See languages/typescript.md for the language first-principles stub.

tsc and tsconfig Discipline (DevOps P2 Automation, DevOps P1 Reproducibility)

  • strict: true is the floor, not the ceiling: it enables strictNullChecks, noImplicitAny, strictFunctionTypes, and more. Disable sub-flags only with a justification comment.
  • tsc --noEmit in CI: type-checking is a build gate; emission is the bundler's job. Separate the two so a type error fails CI even when the bundler would have succeeded.
  • tsconfig is per-project, not inherited verbatim: a shared base (extends) encodes org defaults; each project overrides the deltas it needs. Avoids the "one monoreto-config-fits-all" trap.
  • noUncheckedIndexedAccess for safety: arr[i] becomes T | undefined, forcing narrowing. Costs little, prevents a class of out-of-bounds deref bugs.
  • Applies devops/P1 (reproducibility): pinned typescript version in package.json and lockfile ensure every CI run type-checks against the same compiler.
// tsconfig.json — base
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noEmit": true,
    "moduleResolution": "bundler",
    "isolatedModules": true
  }
}

ESLint and @typescript-eslint (DevOps P2 Automation, Documentation P9 Living Documents)

  • ESLint with @typescript-eslint strict ruleset: recommended-type-checked enables rules that require the type checker (no-floating-promises, no-misused-promises).
  • Rules encode decisions, not taste: every custom rule in the config has a one-line // reason: comment linking to the principle it enforces. This makes the config a living document (Documentation P9).
  • Format is Prettier's job; ESLint lints: eslint-config-prettier disables conflicting format rules. Do not relitigate formatting in code review.
  • no-floating-promises enforces concurrency/P8 (timeout discipline): an un-awaited Promise is a fire-and-forget that swallows errors and timeouts. The rule forces .catch() or await.
// .eslintrc.json
{
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended-type-checked",
    "prettier"
  ],
  "parserOptions": { "project": "./tsconfig.json" },
  "rules": {
    // reason: enforce Concurrency P8 — no un-awaited promises
    "@typescript-eslint/no-floating-promises": "error",
    // reason: enforce Data P7 — no `any` escaping the type checker
    "@typescript-eslint/no-explicit-any": "error"
  }
}

Project References and ts-jest (DevOps P2 Automation, C6 Composability)

  • Project references for monorepos: composite: true + references let tsc --build incrementally type-check only changed projects, and enforce the dependency graph at the type level.
  • paths aliases mirror the import structure: @app/*src/*. Configure once in tsconfig, mirror in the bundler and the test runner so all three agree.
  • ts-jest (or vitest) with isolatedModules: true: each test file is type-checked in isolation, matching how the bundler transpiles. Catches the "passes in tsc but fails in the bundler" gap.
  • Applies devops/P2: the build pipeline (tsc → lint → test → bundle) is automated; a developer never runs a manual sequence.
// tsconfig.references.json
{
  "files": [],
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/api" },
    { "path": "./packages/web" }
  ]
}

Lockfile and Reproducible Install (DevOps P1 Reproducibility)

  • npm ci in CI, not npm install: ci reads the lockfile exactly and fails on drift. install mutates the lockfile.
  • Lockfile committed for applications: for libraries, commit package-lock.json for CI reproducibility even though consumers resolve their own tree.
  • No floating ranges in package.json: ^ and ~ are CI's job to resolve; pin the resolved version in the lockfile. An unpinned * is a supply-chain attack surface.
# CI install step — deterministic
npm ci
# Type-check gate
npx tsc --noEmit
# Lint gate
npx eslint .

Documentation in the Pipeline (Documentation P1 Documentation is Code, DevOps P9 Documentation in the Pipeline)

  • Type-checked JSDoc: typedoc (or TypeDoc) generates API docs from tsdoc comments. The compiler enforces that @param names match real parameters.
  • @example blocks are compiled: a tsdoc @example fenced block is type-checked as part of the doc build. Stale examples fail the pipeline (Documentation P1 — docs are code).
  • README badges reflect CI status: the build/lint/test/type-check gates are the source of truth; badges surface them. Do not hand-edit status tables.
/**
 * Fetch a user by ID.
 *
 * @param id - a branded UserId (see ts-types.md).
 * @throws {NotFoundError} if the user does not exist.
 * @example
 * ```ts
 * const u = await getUser(userId('abc'));
 * ```
 */
async function getUser(id: UserId): Promise<User> { /* ... */ }

Cross-References

  • domains/devops/ci-cd.md — the pipeline gates that host tsc/ESLint/ts-jest.
  • domains/devops/first-principles.md — DevOps P1 Reproducibility, P2 Automation.
  • domains/documentation/first-principles.md — Documentation P1 Documentation is Code.
  • languages/ts-types.md — the type rules ESLint enforces reference this doc.
  • languages/ts-testing.md — the test-runner config (ts-jest/vitest) detailed here.