From dm-lang
Expert TypeScript developer specializing in advanced type system usage, full-stack development, and build optimization. Use PROACTIVELY when working on any TypeScript code - implementing features, reviewing configurations, or debugging type errors, even if not explicitly requested. Applies unless a more specific subagent role overrides.
npx claudepluginhub rbergman/dark-matter-marketplace --plugin dm-langThis skill uses the workspace's default tool permissions.
Senior-level TypeScript expertise for production projects. Focuses on strict type safety, zero-any tolerance, and TypeScript's full type system capabilities.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
Senior-level TypeScript expertise for production projects. Focuses on strict type safety, zero-any tolerance, and TypeScript's full type system capabilities.
tsconfig.json and eslint.config.js for project conventionsRequired:
any - use unknown and narrowas any, as unknown as T)Foundational Principles:
Pin Node version with mise: mise use node@22 (creates .mise.toml — commit it). Team members run mise install. See mise skill for setup.
# Initialize
npm init -y
npm install -D typescript typescript-eslint @eslint-community/eslint-plugin-eslint-comments eslint-plugin-sonarjs prettier lint-staged vitest
# Add scripts to package.json:
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"
# Configure lint-staged (formats only staged files on commit)
npm pkg set lint-staged --json '{"*.{ts,tsx}": ["prettier --write"], "*.{json,yml,yaml}": ["prettier --write"]}'
# Create .prettierignore (prevent formatting machine-generated and non-TS files)
cat > .prettierignore << 'EOF'
# ============================================================================
# DO NOT ADD SOURCE FILES HERE TO WORK AROUND LINE LENGTH LIMITS.
#
# If prettier expansion pushes a file past max-lines (400) or
# max-lines-per-function (60), the file needs to be DECOMPOSED — extract
# functions, split into modules, rearchitect. That is the engineering fix.
#
# Adding source files here suppresses formatting without fixing the real
# problem. The line limits are design signals, not obstacles to route around.
# ============================================================================
# Machine-generated / non-source (safe to exclude)
coverage/
dist/
node_modules/
.worktrees/
.timbers/
.beads/
EOF
# Verify
npm run check
Quality gates run via a git pre-commit hook. Use the shared .githooks/ pattern — hooks are committed to git, every dev/agent gets them on clone.
Setup:
.githooks/ at repo root with executable hook scriptscore.hooksPath to .githooks/ (via just hooks or git config core.hooksPath .githooks).githooks/ to gitDo NOT use bd hooks install or timbers hooks install — they write to .git/hooks/ which git ignores when core.hooksPath is set. Edit .githooks/ directly instead, keeping the same marker block structure so you can copy updated content from future beads/timbers versions.
Pre-commit hook structure (.githooks/pre-commit, four sections in order):
#!/bin/sh
# --- BEGIN BEADS INTEGRATION --- (copy from bd hooks install output)
# ... beads hook content ...
# --- END BEADS INTEGRATION ---
# Beads sync — export state + stage for commit
bd export -o .beads/issues.jsonl 2>/dev/null
git add -f .beads/issues.jsonl 2>/dev/null
# Quality gates
npx lint-staged
npm run check
# --- BEGIN TIMBERS --- (copy from timbers hooks install output)
# ... timbers hook content ...
# --- END TIMBERS ---
Post-merge hook (.githooks/post-merge):
#!/bin/sh
# Import beads state from incoming changes
bd import 2>/dev/null
Why this order: Beads runs first (fast, no deps). Export+stage captures mutations from the current session. Quality gates run last (slowest, may fail). Timbers is post-gate.
Justfile recipes:
hooks:
@current=$(git config --get core.hooksPath 2>/dev/null || true); \
if [ "$current" = ".githooks" ]; then \
echo " hooks already configured"; \
else \
git config core.hooksPath .githooks; \
echo " ✅ core.hooksPath set to .githooks"; \
fi
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
# Move hook content to .githooks/, set core.hooksPath
Do not use bd hooks install --beads or --shared — these point core.hooksPath at beads-owned directories (.beads/hooks/, .beads-hooks/). The .githooks/ pattern is repo-owned, which is the key difference.
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).
# Root package.json (npm workspaces / turborepo):
npm pkg set lint-staged --json '{"packages/web/**/*.{ts,tsx}": ["prettier --write"], "*.{json,yml,yaml}": ["prettier --write"]}'
# Or independent packages (no workspaces): install lint-staged per TS package
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.
# .githooks/pre-commit (after beads markers):
bd export -o .beads/issues.jsonl 2>/dev/null
git add -f .beads/issues.jsonl 2>/dev/null
npx lint-staged
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.
git clone <repo> && cd <repo>
just setup # Runs mise trust/install + npm ci
just check # Verify everything works
Or manually:
mise trust && mise install # Get pinned Node version
npm ci # Get dependencies
Why strict configs? Type errors caught at compile time are 10x cheaper than runtime bugs. Strict linting prevents any from leaking through the codebase.
Invoke the just-pro skill for build system setup. It covers:
references/package-ts.just)Alternative: Use npm scripts directly if just is unavailable.
Auto-Fix First - Always try auto-fix before manual fixes:
npx prettier --write src/ # Format changed files
npx eslint src/ --fix # Fixes style, imports, etc.
npx tsc --noEmit # Type check without emit
Verification:
npm run check # typecheck + lint + test
npm audit --omit=dev --audit-level=high # vulnerability check (production deps only)
Or via just (which combines both):
just check
Pre-commit Hook (git hook with lint-staged):
npm run check runs typecheck + lint + test.git/hooks/pre-commit alongside beads/timbers hooks (not husky).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 contentWhen creating a new project, use this complete template — omitting rules allows any to leak through the codebase.
import tseslint from 'typescript-eslint';
import eslintComments from '@eslint-community/eslint-plugin-eslint-comments';
import sonarjs from 'eslint-plugin-sonarjs';
export default tseslint.config(
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
sonarjs.configs.recommended,
{
files: ['src/**/*.ts', 'src/**/*.tsx'],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
'@eslint-community/eslint-comments': eslintComments,
},
rules: {
// === TYPE SAFETY (non-negotiable) ===
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unsafe-argument': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-call': 'error',
'@typescript-eslint/no-unsafe-member-access': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/no-unsafe-type-assertion': 'error',
'@typescript-eslint/no-non-null-assertion': 'error',
// === PROMISES ===
'@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true, ignoreIIFE: true }],
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/require-await': 'error',
'@typescript-eslint/promise-function-async': 'error',
// === COMPLEXITY LIMITS ===
// These limits exist to trigger EXTRACTION into well-named companion
// files/functions — NOT to compress code, remove comments, combine
// statements, or shorten names. When violated, decompose by responsibility.
'complexity': ['error', { max: 10 }],
'sonarjs/cognitive-complexity': ['error', 15],
'max-depth': ['error', 4],
'max-len': ['error', { code: 120, ignoreUrls: true, ignoreStrings: false, ignoreTemplateLiterals: false, ignoreRegExpLiterals: true }],
'max-lines-per-function': ['error', { max: 60, skipBlankLines: true, skipComments: true }],
'max-lines': ['error', { max: 400, skipComments: true }],
'max-params': ['error', 4],
// === BLOCK DISABLING CRITICAL RULES ===
'@eslint-community/eslint-comments/no-restricted-disable': ['error',
'@typescript-eslint/no-explicit-any',
'@typescript-eslint/no-unsafe-assignment',
'@typescript-eslint/no-unsafe-argument',
'@typescript-eslint/no-floating-promises',
'complexity', 'sonarjs/cognitive-complexity', 'max-len', 'max-lines-per-function', 'max-lines',
],
'@eslint-community/eslint-comments/require-description': ['error', { ignore: ['eslint-enable'] }],
// === COMMENTS ===
'@typescript-eslint/ban-ts-comment': ['error', {
'ts-expect-error': 'allow-with-description',
'ts-ignore': true,
'ts-nocheck': true,
minimumDescriptionLength: 10,
}],
// === CONSISTENCY ===
'@typescript-eslint/explicit-module-boundary-types': 'error',
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
},
},
// Relax for tests
{
files: ['**/*.test.ts', '**/*.spec.ts'],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'max-lines-per-function': 'off',
'max-lines': 'off',
'complexity': 'off',
'sonarjs/cognitive-complexity': 'off',
'@eslint-community/eslint-comments/no-restricted-disable': 'off',
},
},
{ ignores: ['dist/', 'node_modules/', 'coverage/', '*.js', '*.cjs', '*.mjs'] },
);
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:
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:
max-len at 120 catches this — the line limit and file limit work together).prettierignore so prettier won't expand them backAny 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.
| 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.
| 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 |
| 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 |
inferT[K])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.
as any or as unknown as T type assertions@ts-ignore instead of @ts-expect-error with reasoneslint-disable to bypass type safety or complexity rules (blocked by config)x! operator) instead of proper narrowingReact 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.
Before writing code:
tsconfig.json for compiler options and strict settingseslint.config.js for project-specific lint rulesWhen writing code:
unknown and narrow with type guards - never anyBefore committing:
just check (includes typecheck + lint + test + vulnerability audit)npm run check && npm audit --omit=dev --audit-level=highjust check or turbo run checknpx eslint src/ --fix && npx tsc --noEmit && npm test