| name | typescript-pro |
| description | Strict TypeScript with zero-any tolerance, no-unsafe-* lints, floating-promise prevention, and disciplined type-system usage. Use when implementing, debugging, refactoring, or reviewing TypeScript code; resolving type errors; configuring tsconfig/ESLint/Prettier; setting up React/Next/Express patterns; eliminating any/unknown drift; or evaluating advanced generics, conditional types, and inference. Applies to any TypeScript work unless a more specific role overrides. |
TypeScript Pro
Senior-level TypeScript expertise for production projects. Focuses on strict type safety, zero-any tolerance, and TypeScript's full type system capabilities.
When Invoked
- Review
tsconfig.json and eslint.config.js for project conventions
- For build system setup, invoke the just-pro skill (covers just vs make)
- Apply type-first development and established project patterns
Core Standards
Required:
- Strict mode enabled with all compiler flags
- NO explicit or implicit
any - use unknown and narrow
- NO type assertions to circumvent the type system (
as any, as unknown as T)
- NO dangling promises - await, return, or void explicitly
- All exported functions have explicit return types
- ESLint strict-type-checked passes with project configuration
- Table-driven tests for multiple cases
Foundational Principles:
- Single Responsibility: One module = one purpose, one function = one job
- No God Objects: Split large classes/objects; if it has 10+ methods or properties, decompose
- Dependency Injection: Pass dependencies via constructor/params, don't instantiate internally
- Small Interfaces: Prefer many small types over few large ones; compose with intersection types
- Composition over Inheritance: Use object composition and mixins, not deep class hierarchies
Project Setup (TypeScript 5.5+)
Version Management
Pin Node version with mise: mise use node@22 (creates .mise.toml — commit it). Team members run mise install. See mise skill for setup.
New Project Quick Start
npm init -y
npm install -D typescript typescript-eslint @eslint-community/eslint-plugin-eslint-comments eslint-plugin-sonarjs prettier lint-staged vitest
npm pkg set scripts.typecheck="tsc --noEmit"
npm pkg set scripts.lint="eslint src/"
npm pkg set scripts.test="vitest run"
npm pkg set scripts.check="npm run typecheck && npm run lint && npm run test"
npm pkg set lint-staged --json '{"*.{ts,tsx}": ["prettier --write"], "*.{json,yml,yaml}": ["prettier --write"]}'
cat > .prettierignore << 'EOF'
coverage/
dist/
node_modules/
.worktrees/
.timbers/
.beads/
EOF
npm run check
Pre-commit Hook
Quality gates run via a git pre-commit hook. With beads 1.0+, hooks live in .beads/hooks/ (committed to git, managed by bd hooks install --beads). Beads, timbers, and your quality gates all coexist in the same hook file via section markers — content outside markers is preserved across reinstalls.
Setup:
bd init (creates .beads/hooks/ and sets core.hooksPath = .beads/hooks)
timbers hooks install (detects core.hooksPath, appends into existing files alongside beads)
- Add quality gates between the BEADS and TIMBERS marker sections — they're preserved across
bd hooks install --force and timbers hooks install reruns
Pre-commit hook structure (.beads/hooks/pre-commit):
#!/usr/bin/env sh
if [ -f .git/MERGE_HEAD ]; then
echo "Merge commit — skipping lint-staged"
else
npx lint-staged
fi
if command -v just >/dev/null 2>&1 && [ -f justfile ]; then
just check
else
npm run check
fi
Why this order: Beads runs first (fast: bd's internal hook handles auto-export+stage of .beads/issues.jsonl). Quality gates run next (slowest, may fail — but bd export already happened, so beads state is captured even if gates fail and the commit aborts). Timbers runs last (post-gate, post-export).
Auto-export defaults (beads 1.0+):
export.auto = true — every bd mutation writes .beads/issues.jsonl (60s throttled)
export.git-add = true — auto-stages it
- The pre-commit hook forces a flush, so commits always carry current state
Justfile recipe:
hooks:
@bd hooks install --force --beads >/dev/null && echo " ✅ beads hooks installed"
@if command -v timbers >/dev/null 2>&1; then \
timbers hooks install >/dev/null && echo " ✅ timbers hooks installed"; \
fi
@current=$(git config --get core.hooksPath 2>/dev/null || true); \
if [ "$current" != ".beads/hooks" ]; then \
git config core.hooksPath .beads/hooks; \
echo " ✅ core.hooksPath fixed to .beads/hooks (was $current)"; \
fi
Note on core.hooksPath: bd hooks install --force may set this to an absolute path. Force it relative — worktrees share repo config, and an absolute path won't resolve from a worktree's working dir.
New dev/agent onboarding: git clone <repo> && just setup (which includes just hooks).
If a project currently uses husky, migrate:
npm uninstall husky && rm -rf .husky && npm pkg delete scripts.prepare
bd hooks install --force --beads
git config core.hooksPath .beads/hooks
Monorepo Variant
In monorepos (multiple packages, possibly mixed languages), adjust the setup:
lint-staged: scoped to TS packages only. Don't format Go/Rust code with Prettier — they have their own formatters (goimports, rustfmt).
npm pkg set lint-staged --json '{"packages/web/**/*.{ts,tsx}": ["prettier --write"], "*.{json,yml,yaml}": ["prettier --write"]}'
Pre-commit: lint-staged only, no npm run check. Full quality gates across all packages are too slow for pre-commit. Run lint-staged in the hook, run full gates via just check or CI.
npx lint-staged
(beads 1.0+ auto-exports + auto-stages .beads/issues.jsonl on every mutation; the manual export+stage lines older docs showed are no longer needed.)
For mixed-language monorepos without workspaces, detect which packages have staged files:
if git diff --cached --name-only | grep -q '^packages/web/'; then
(cd packages/web && npx lint-staged)
fi
.prettierrc: root-level. Prettier walks up the directory tree, so a single root config covers all TS packages. Use per-package configs only if packages need different formatting.
Required Config Files: Copy references/gitignore → .gitignore, references/prettierrc.json → .prettierrc, then create tsconfig.json and eslint.config.js per the templates below.
Developer Onboarding
git clone <repo> && cd <repo>
just setup
just check
Or manually:
mise trust && mise install
npm ci
Why strict configs? Type errors caught at compile time are 10x cheaper than runtime bugs. Strict linting prevents any from leaking through the codebase.
Build System
Invoke the just-pro skill for build system setup. It covers:
- Simple repos vs monorepos
- Hierarchical justfile modules
- TypeScript-specific templates (
references/package-ts.just)
Alternative: Use npm scripts directly if just is unavailable.
Quality Assurance
Auto-Fix First - Always try auto-fix before manual fixes:
npx prettier --write src/
npx eslint src/ --fix
npx tsc --noEmit
Verification:
npm run check
npm audit --omit=dev --audit-level=high
Or via just (which combines both):
just check
Pre-commit Hook (git hook with lint-staged):
- lint-staged formats only staged files via Prettier (no whole-repo formatting)
- Then
npm run check runs typecheck + lint + test
- Blocks commits with formatting issues, type errors, lint violations, or failing tests
- Lives in
.beads/hooks/pre-commit alongside beads/timbers hooks (not husky, not .git/hooks/)
.prettierignore must exclude .timbers/ and .beads/ — without this, lint-staged reformats timbers JSON during commit, but its stash/restore cycle puts the original format back in the working tree, creating perpetual MM diffs with no semantic content
Linting Configuration
eslint.config.js Template
When creating a new project, copy references/eslint.config.js from this skill — it's the canonical template. Omitting rules allows any to leak through the codebase.
The template enforces four rule families:
| Family | Purpose | Key rules |
|---|
| Type safety | Block any and unsafe-* drift | no-explicit-any, no-unsafe-{argument,assignment,call,member-access,return,type-assertion}, no-non-null-assertion |
| Promises | Catch unhandled async | no-floating-promises, no-misused-promises, require-await, promise-function-async |
| Complexity (extraction signal) | Force decomposition by responsibility, not compression | complexity: 10, sonarjs/cognitive-complexity: 15, max-depth: 4, max-len: 120, max-lines-per-function: 60, max-lines: 400, max-params: 4 |
| Disable governance | Stop ad-hoc disabling of critical rules | @eslint-community/eslint-comments/no-restricted-disable blocks disabling type-safety, promise, and complexity rules without an explicit override |
Tests (**/*.test.ts, **/*.spec.ts) relax any, complexity, and line limits.
Responding to Limit Violations
These limits exist to improve code architecture, not to be gamed. When a file or function exceeds a limit, the correct response is to decompose by responsibility — not to make the code fit by any means necessary.
Extract, don't compress:
- Identify logical sections (validation, transformation, formatting, I/O)
- Extract each into a well-named function or module — the function name itself documents what the section does
- Place extracted code in a companion file in the same directory (e.g.,
order-service.ts → order-validation.ts, order-transforms.ts)
When extraction is costly (many locals to pass), use a context/options object. If splitting would duplicate state, the code may need a different decomposition axis (by entity rather than by phase).
Prohibited responses to limit violations:
- Combining statements onto single lines to dodge file/function length limits (
max-len at 120 catches this — the line limit and file limit work together)
- Removing or shortening comments
- Compressing whitespace or collapsing readable formatting
- Shortening descriptive variable/function names
- Inlining helper functions to reduce function count
- Adding source files to
.prettierignore so prettier won't expand them back
Any of these trades one problem (length) for a worse one (readability). The goal is clean architecture, not metric compliance. Prettier enforces consistent formatting, so compressed code will be expanded back to its readable form — and max-len prevents the line-combining workaround entirely. Extraction is the only sustainable fix.
Enforced Limits
| Limit | Value | Purpose |
|---|
max-len | 120 chars | Prevent line-combining to dodge file/function limits |
max-lines | 400 code | Prevent god modules (comments excluded) |
max-lines-per-function | 60 | Single responsibility |
complexity | 10 | Cyclomatic complexity (branching paths) |
sonarjs/cognitive-complexity | 15 | Cognitive complexity (perceived difficulty) |
max-depth | 4 | Avoid arrow code |
max-params | 4 | Use options objects |
Critical rules cannot be disabled via eslint-disable comments - the config blocks it.
Quick Reference
Type Safety Patterns
| Pattern | Use |
|---|
unknown over any | Safe default for unknown types |
| Type guards | Runtime narrowing with type safety |
| Discriminated unions | State machines, tagged unions |
| Branded types | Domain modeling (UserId vs string) |
satisfies operator | Validate without widening |
as const | Literal types from values |
Error Handling
| Pattern | Use |
|---|
Result<T, E> type | Explicit success/failure |
never exhaustive check | Catch unhandled cases |
| Custom error classes | Typed error discrimination |
| Zod validation | Runtime + compile-time safety |
Type System Techniques
- Generic constraints and variance
- Conditional types with
infer
- Mapped types with modifiers
- Template literal types
- Index access types (
T[K])
Project Organization
project/
├── src/
│ ├── index.ts # Entry point / exports
│ ├── types/ # Shared type definitions
│ └── lib/ # Implementation
├── tsconfig.json
├── eslint.config.js
├── package.json
└── justfile
Rules: One module = one purpose. Use barrel exports sparingly. Avoid circular dependencies.
Anti-Patterns
as any or as unknown as T type assertions
@ts-ignore instead of @ts-expect-error with reason
- Disabling strict checks to fix errors
- Using
eslint-disable to bypass type safety or complexity rules (blocked by config)
- Implicit any in function parameters
- Dangling promises without await/void
- Over-complicated generic signatures
- Non-null assertions (the
x! operator) instead of proper narrowing
- Truthy/falsy checks on non-booleans
- Functions over 60 lines or files over 400 lines (extract into companion files, don't compress)
- God classes/objects with 10+ methods or properties
- Deep inheritance hierarchies (prefer composition)
- Barrel files that re-export everything (causes circular deps)
Framework-Specific
React 19+: Explicit props typing (avoid FC), use satisfies for configs.
Next.js: Type server components, use Metadata types, type API routes.
Express/Fastify: Type request handlers, use generic route parameters.
See references/integration.md for detailed framework patterns.
AI Agent Guidelines
Before writing code:
- Read
tsconfig.json for compiler options and strict settings
- Check
eslint.config.js for project-specific lint rules
- Identify existing type patterns in the codebase to follow
When writing code:
- Start with type definitions before implementation
- Use
unknown and narrow with type guards - never any
- Handle all promise returns explicitly
- Add explicit return types to exported functions
Before committing:
- Run
just check (includes typecheck + lint + test + vulnerability audit)
- Fallback:
npm run check && npm audit --omit=dev --audit-level=high
- For monorepos at repo root:
just check or turbo run check
- Fallback:
npx eslint src/ --fix && npx tsc --noEmit && npm test