
type-hardening
by BerryKuipers
Universal Claude Code configuration: agents, skills, workflows, and SessionStart hooks for consistent development across projects
SKILL.md
name: type-hardening
description: Incrementally improve type safety by replacing string literals with enums, narrowing any types, and using shared types. Works in small verified batches. Preserves functionality while improving code quality.
Type Hardening Skill
Purpose
Gradually improve type safety by:
- Replacing magic strings with enums/constants
- Narrowing
any/unknownto specific types - Using shared types instead of inline definitions
- Adding type imports where missing
When to Use
- Code review finds string literals that should be enums
anytypes that can be narrowed- Inline types duplicating shared definitions
- Post-refactoring type cleanup
- Technical debt reduction sprints
- Triggered by
/harden-typescommand or/refactor --strategy=types
Core Principles
- Check Existing First: Always search for existing types before creating new ones
- Small Batches: 1-3 related changes at a time
- Verify Immediately: Type check after every change
- No Logic Changes: Purely structural/type improvements
- Commit Often: Atomic commits per successful batch
- New Types Allowed: Create new types when genuinely needed for clean code
- No
unknownCheating: Replacinganywithunknownis NOT an improvement - narrow to a SPECIFIC type or leave it (with a TODO comment if needed)
Instructions
Step 1: Discover Existing Types
Before ANY changes, locate existing type definitions:
# Find Prisma enums (common source of types)
grep -E "^enum " backend/prisma/schema.prisma 2>/dev/null || true
# Find shared types
find . -path ./node_modules -prune -o -name "*.types.ts" -print 2>/dev/null
find . -path ./node_modules -prune -o -name "types.ts" -print 2>/dev/null
# Find constants files
find . -path ./node_modules -prune -o -name "*.constants.ts" -print 2>/dev/null
# Common locations to check:
# - shared/types/
# - src/types/
# - backend/src/types/
# - backend/src/generated/prisma/ (Prisma enums)
Record findings before proceeding.
Step 2: Analyze Target File
Read the target file and identify:
### Type Hardening Opportunities
1. **String Literals** → Should use enum/constant
- Line 45: `'admin'` → `UserRole.admin`
- Line 89: `'pending'` → `JobStatus.PENDING`
2. **Any Types** → Should be narrowed to SPECIFIC types
- Line 23: `any` → `UserWithRole` ✅
- Line 67: `any` → `ApiResponse<T>` ✅
- ❌ WRONG: `any` → `unknown` (this is cheating, not fixing)
3. **Inline Types** → Should use shared
- Line 102: `{ id: string; name: string }` → `UserBasic`
4. **Missing Imports** → Need to add
- `UserRole` from `@prisma/client` or generated types
Step 3: Prioritize Changes
Work in this order for safest progression:
-
Prisma Enums First (most reliable)
// ❌ BEFORE if (user.role === 'admin') // ✅ AFTER import { UserRole } from '../generated/prisma/index.js'; if (user.role === UserRole.admin) -
Shared Types Second (well-established)
// ❌ BEFORE type NotificationType = 'info' | 'success' | 'warning' | 'error'; // ✅ AFTER import type { NotificationType } from '@shared/types/notification'; -
Constants Third (project-specific)
// ❌ BEFORE const provider = 'openai'; // ✅ AFTER import { AI_PROVIDERS } from '../types/ai.constants'; const provider = AI_PROVIDERS.OPENAI; -
New Types Last (when genuinely needed)
// Only create new types when: // - No existing type serves the purpose // - The type will be used in multiple places // - It improves code clarity significantly
Step 4: Apply Changes (Batch of 1-3)
# Edit the file with precise changes
# Use Edit tool for exact string replacement
Example transformation:
// Before (lines 44-48)
async function promoteUser(userId: string): Promise<void> {
const user = await userRepo.findById(userId);
if (user.role === 'user') {
await userRepo.update(userId, { role: 'admin' });
}
}
// After
import { UserRole } from '../generated/prisma/index.js';
async function promoteUser(userId: string): Promise<void> {
const user = await userRepo.findById(userId);
if (user.role === UserRole.user) {
await userRepo.update(userId, { role: UserRole.admin });
}
}
Step 5: Verify Immediately
# Backend files
cd backend && npx tsc --noEmit
# Frontend files
npx tsc --noEmit
# If using ESLint
npx eslint <modified-file>
If verification fails:
- ❌ DO NOT proceed to next batch
- Analyze the error
- Fix or revert the change
- Re-verify before continuing
Step 6: Commit Atomic Change
git add <modified-file>
git commit -m "refactor: harden types in <filename>
- Replace string literals with <EnumName>
- Narrow any to <TypeName>
- No behavior change
Co-Authored-By: Claude <noreply@anthropic.com>"
Step 7: Iterate or Complete
Continue if:
- More hardening opportunities exist
- User requested multiple files
- Within iteration limit
Stop if:
- File is clean (no more opportunities)
- Verification failed (fix first)
- User requested stop
- Iteration limit reached
Output Format
Progress Report
## Type Hardening Report: `backend/src/services/auth.service.ts`
### Changes Applied
| Line | Before | After | Status |
|------|--------|-------|--------|
| 45 | `'admin'` | `UserRole.admin` | ✅ |
| 89 | `'user'` | `UserRole.user` | ✅ |
| 123 | `any` | `AuthPayload` | ✅ |
### Verification
- TypeScript: ✅ 0 errors
- Lint: ✅ passed
### Remaining Opportunities
- Line 156: `'pending'` could use `JobStatus.PENDING`
- Line 201: `unknown` could narrow to `TokenPayload`
### Next Action
Continue? (y/n/skip to file X)
Integration Points
With Refactor Agent
Referenced as Pattern 6 in /refactor:
### Pattern 6: Type Hardening
**When**: String literals, `any` types, inline types matching shared
**How**: Use `type-hardening` skill
**Reference**: `.claude/skills/quality/type-hardening/SKILL.md`
With Quality Gate
Can be part of quality checks:
### Optional: Type Strictness Check
Use `type-hardening` skill in analysis-only mode to identify
opportunities without making changes.
With Gemini Delegation
Safe for delegation with guardrails:
## Gemini Handoff: Type Hardening
Scope: [specific files]
Task: Replace string literals with existing enums
Verification: tsc --noEmit must pass
Common Type Sources
| Source | Location | Example |
|---|---|---|
| Prisma Enums | backend/src/generated/prisma/ | UserRole, JobStatus |
| Shared Types | shared/types/ | NotificationType, ApiResponse |
| Domain Types | src/types/ | UserWithRole, SceneData |
| Constants | */constants.ts | AI_PROVIDERS, CACHE_TTL |
Error Recovery
Import Resolution Failed
# Check if type is exported
grep "export.*TypeName" shared/types/
# Check tsconfig paths
cat tsconfig.json | grep -A5 "paths"
Type Mismatch After Change
# Revert the change
git checkout -- <file>
# Analyze why it failed
# - Was the enum value different? (admin vs ADMIN)
# - Was the type path wrong?
# - Was there a missing export?
Best Practices
- Never guess - Always verify the type exists before using it
- Match casing -
UserRole.adminnotUserRole.ADMIN(check actual enum) - Preserve semantics -
'admin'becomesUserRole.admin, notRole.ADMIN - Import correctly - Use
import typefor type-only imports - Test critical paths - Extra verification for auth/security code
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon