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