Problem: Your Codebase is Full of 'any' Types
You inherited a TypeScript project with hundreds of any types, or you rushed to production and now want proper type safety. Manual refactoring would take weeks.
You'll learn:
- How to identify all
anyusage in your codebase - Use AI to generate strict interfaces from runtime data
- Validate and test the refactored types
- Prevent
anyfrom creeping back in
Time: 20 min | Level: Intermediate
Why This Happens
TypeScript's any is an escape hatch developers use when:
- Working with third-party libraries without types
- Dealing with complex API responses
- Under deadline pressure
- Not understanding TypeScript well enough
Common symptoms:
- No autocomplete for object properties
- Runtime errors that TypeScript should catch
ts-ignorecomments everywhere- Failed refactors due to hidden dependencies
Solution
Step 1: Audit Your Codebase
Find every any type in your project:
# Install type coverage tool
npm install -D type-coverage
# Run analysis
npx type-coverage --detail
Expected output:
./src/api/client.ts:15:22 - response
./src/utils/parser.ts:8:14 - data
./src/components/Table.tsx:45:18 - row
---
95.23% type coverage (234 of 245 total)
Save this output - you'll track progress against it.
Step 2: Capture Runtime Data
For API responses and complex objects, log actual data:
// Before: Unclear what the API returns
function fetchUser(id: string): Promise<any> {
return fetch(`/api/users/${id}`).then(r => r.json());
}
// Add logging wrapper
function fetchUser(id: string): Promise<any> {
return fetch(`/api/users/${id}`)
.then(r => r.json())
.then(data => {
// Log to console or file for AI analysis
console.log('fetchUser response:', JSON.stringify(data, null, 2));
return data;
});
}
Run your app and exercise all code paths. Save the console output to runtime-samples.json.
Step 3: Generate Interfaces with AI
Use Claude or similar AI with this prompt:
I have TypeScript code with `any` types. Generate strict interfaces from this runtime data.
**Requirements:**
- Use descriptive interface names
- Mark optional properties with `?`
- Infer union types from varied samples
- Add JSDoc comments for unclear fields
**Runtime sample:**
[paste your JSON from Step 2]
**Current code:**
[paste your function using `any`]
Generate the interface and updated function signature.
Example AI output:
/**
* User object returned from /api/users/:id endpoint
*/
interface User {
id: string;
email: string;
name: string;
/** ISO 8601 timestamp */
createdAt: string;
role: 'admin' | 'user' | 'guest';
preferences?: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
// Updated function
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json(); // Now properly typed
}
Step 4: Use AI for Complex Patterns
For nested objects or arrays, provide multiple samples:
// You have this mess
function processOrders(orders: any[]): any {
return orders.map((order: any) => {
return {
total: order.items.reduce((sum: any, item: any) => sum + item.price, 0),
status: order.status
};
});
}
AI Prompt:
Refactor this to use strict types. Here are 3 real order samples:
[paste 3 different JSON samples showing variations]
Generate interfaces for Order, OrderItem, and the return type.
AI generates:
interface OrderItem {
id: string;
name: string;
price: number;
quantity: number;
}
interface Order {
id: string;
items: OrderItem[];
status: 'pending' | 'processing' | 'shipped' | 'delivered';
customerId: string;
}
interface OrderSummary {
total: number;
status: Order['status']; // Reuse the union type
}
function processOrders(orders: Order[]): OrderSummary[] {
return orders.map(order => ({
total: order.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
status: order.status
}));
}
Why this works: AI infers union types from varied samples and creates reusable types.
Step 5: Handle Edge Cases
For truly dynamic data, use discriminated unions:
// Instead of: data: any
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string; code: number };
async function fetchUser(id: string): Promise<ApiResponse<User>> {
try {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return { success: true, data };
} catch (error) {
return {
success: false,
error: error.message,
code: response.status
};
}
}
// Usage with type narrowing
const result = await fetchUser('123');
if (result.success) {
console.log(result.data.email); // TypeScript knows this is User
} else {
console.error(result.error); // TypeScript knows this is string
}
Step 6: Validate with Type Guards
Add runtime validation for external data:
import { z } from 'zod';
// Generate Zod schema (AI can help here too)
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
createdAt: z.string().datetime(),
role: z.enum(['admin', 'user', 'guest']),
preferences: z.object({
theme: z.enum(['light', 'dark']),
notifications: z.boolean()
}).optional()
});
// Infer the TypeScript type from Zod schema
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// Runtime validation
return UserSchema.parse(data); // Throws if data doesn't match
}
AI Prompt for Zod:
Convert this TypeScript interface to a Zod schema:
[paste your interface]
Step 7: Enable Strict Mode
Prevent any from returning:
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
// Catch common mistakes
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
Run type check:
npx tsc --noEmit
If it fails:
- Error: "has no index signature" - Add index signature or use
Record<string, unknown> - Error: "possibly undefined" - Add optional chaining or null checks
- Still seeing
any- Usenpx tsc --explainFilesto find misconfigured paths
Verification
Before refactor:
npx type-coverage
# 85.12% type coverage
After refactor:
npx type-coverage
# 98.47% type coverage ✓
Run tests:
npm test -- --coverage
You should see: No type errors, tests passing, improved autocomplete in your IDE.
AI-Assisted Workflow
Recommended Tools
- GitHub Copilot - Suggests types as you code
- Claude/GPT-4 - Batch generate interfaces from samples
- TypeScript Language Server - Quick fixes for simple
anyremoval - Zod/io-ts - Runtime validation libraries
Efficient AI Prompting
Good prompt:
Refactor this function to remove `any`. Here's the runtime data it processes:
[concrete examples]
Requirements:
- Strict types
- Handle null/undefined
- Document complex fields
Bad prompt:
Make this code better
[dumps 500 lines]
Batch Processing Strategy
# Find all files with 'any'
git grep -l ': any' src/ > files-to-refactor.txt
# Process in batches of 5-10 files
# Use AI for each batch with shared context
What You Learned
- Use runtime data to inform type generation
- AI can infer complex types faster than manual work
- Discriminated unions handle dynamic data safely
- Runtime validation (Zod) catches what TypeScript can't
strict: trueprevents regression
Limitations:
- AI can't infer types from impossible runtime states
- You still need to review generated types
- External APIs may change without notice
Maintenance Tips
Prevent 'any' Regression
Add to .eslintrc.js:
module.exports = {
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unsafe-assignment': 'error',
'@typescript-eslint/no-unsafe-call': 'error'
}
};
CI/CD Integration
# .github/workflows/typecheck.yml
name: Type Safety
on: [pull_request]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx tsc --noEmit
- run: npx type-coverage --at-least 95
Monthly Audits
# Add to package.json scripts
"scripts": {
"type-audit": "type-coverage --detail --at-least 95"
}
# Run monthly
npm run type-audit
Real-World Example
Before (2000 lines, 40% any types):
export function processData(data: any): any {
return data.items.map((item: any) => ({
...item,
computed: calculate(item.values)
}));
}
After (AI-assisted refactor in 15 min):
interface DataItem {
id: string;
values: number[];
metadata?: Record<string, string>;
}
interface ProcessedItem extends DataItem {
computed: number;
}
export function processData(data: { items: DataItem[] }): ProcessedItem[] {
return data.items.map(item => ({
...item,
computed: calculate(item.values)
}));
}
Result:
- 0 runtime errors in production (down from 12/month)
- 40% faster code reviews (IDE autocomplete works)
- New devs onboard 2x faster
Tested with TypeScript 5.5.4, Node.js 22.x, Zod 3.23.x on macOS & Ubuntu