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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/dm-lang:typescript-proThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Senior-level TypeScript expertise for production projects. Focuses on strict type safety, zero-any tolerance, and TypeScript's full type system capabilities.
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 testnpx claudepluginhub rbergman/dark-matter-marketplace --plugin dm-langTypeScript deep-dive skill covering type-level programming, performance optimization, monorepo patterns, and migration strategies. Also redirects to specialized subagents for bundler/module/type issues.
Provides advanced TypeScript/JavaScript expertise for type-level programming, performance optimization, monorepo management, migrations, and modern tooling. Analyzes projects, applies fixes, and validates with typechecks/tests.
Sets up strict ESLint config for TypeScript projects and systematically fixes all linting issues via auto-fix and manual remediation.