Handling “any” in Legacy Code¶
any is both a blessing and a curse.
Why any exists¶
- It allows gradual migration
- It keeps JS interop easy
- It prevents TypeScript from blocking you
Why any is dangerous¶
- It disables type checking
- It hides bugs
- It spreads through your code like a virus
- It makes refactoring risky
Your goal is not to eliminate any instantly, but to control it and reduce it over time.
Strategies to Reduce any¶
Here are the most effective, real‑world strategies for taming any.
1. Replace any with unknown¶
unknown is the safe version of any.
let value: unknown = getLegacyValue()
You must narrow it before using it:
if (typeof value === "string") {
console.log(value.toUpperCase())
}
When to use unknown¶
- external data
- JSON parsing
- untyped libraries
- dynamic values
This prevents accidental misuse.
2. Add Type Annotations at the Boundaries¶
You don’t need to type everything—just the edges:
- function parameters
- return types
- API responses
- module exports
Example¶
function parseUser(data: any): User {
return {
id: data.id,
name: data.name
}
}
Even if data is any, the output is typed.
This stops any from leaking further.
3. Use Generics Instead of any¶
Replace this:
function wrap(value: any): any {
return value
}
With this:
function wrap<T>(value: T): T {
return value
}
Generics preserve type information and eliminate any entirely.
4. Use Utility Types to Shape Legacy Data¶
Legacy objects often have partial or inconsistent shapes.
Use:
Partial<T>Pick<T, K>Omit<T, K>Record<K, T>
Example¶
function updateUser(id: string, data: Partial<User>) { ... }
This avoids any while staying flexible.
5. Add JSDoc Types to JS Files¶
If you can’t convert a file to TypeScript yet, add JSDoc:
/**
* @param {string} name
* @returns {number}
*/
function getLength(name) {
return name.length
}
With checkJs: true, TypeScript will type‑check this JS file.
This is a great way to reduce any without renaming files.
6. Use as const to Prevent Widening¶
Legacy code often widens types unnecessarily:
const status = "success" // type: string
Fix:
const status = "success" as const // type: "success"
This prevents string from becoming any in unions.
7. Add Types for Third‑Party Libraries¶
If a library returns any, install or write types:
npm install --save-dev @types/some-lib
Or create:
types/some-lib/index.d.ts
This stops any from leaking into your code.
8. Replace any with Narrower Types¶
Even if you don’t know the exact type, you can often narrow it:
Instead of:
let data: any
Use:
let data: object
let data: string[]
let data: Record<string, unknown>
let data: unknown[]
Every bit of narrowing helps.
9. Use ESLint to Prevent New any¶
Add this rule:
"@typescript-eslint/no-explicit-any": "warn"
Or stricter:
"@typescript-eslint/no-explicit-any": "error"
This prevents new any from creeping in.
10. Use noImplicitAny Once the Codebase Is Ready¶
This is the final step.
Enable:
"noImplicitAny": true
This forces you to type everything that would otherwise become any.
Do this only after reducing the worst offenders.
Progressive Strictness¶
TypeScript allows you to increase strictness gradually.
Here’s the recommended order:
Phase 1 — Safe Migration¶
Enable:
"strict": false
"allowJs": true
"checkJs": false
Goal: compile successfully.
Phase 2 — Start Catching Bugs¶
Enable:
"checkJs": true
"noImplicitAny": false
Goal: type‑check JS files.
Phase 3 — Improve Type Safety¶
Enable:
"strictNullChecks": true
"noUncheckedIndexedAccess": true
Goal: catch null/undefined bugs.
Phase 4 — Full Strict Mode¶
Enable:
"strict": true
Goal: maximum type safety.
Putting It All Together¶
Here’s a realistic example of reducing any in a legacy function.
Before¶
function loadUser(id: any): any {
const data = fetch(`/user/${id}`)
return JSON.parse(data)
}
After (incremental improvements)¶
function loadUser(id: string): Promise<unknown> {
return fetch(`/user/${id}`).then(res => res.json())
}
Then add a type guard:
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value
)
}
Finally:
async function loadUser(id: string): Promise<User> {
const data = await fetch(`/user/${id}`).then(res => res.json())
if (!isUser(data)) {
throw new Error("Invalid user data")
}
return data
}
Zero any.
Full type safety.
No breaking changes.
Summary¶
In this lesson, you learned how to handle and reduce any in legacy codebases:
1. Strategies to reduce any¶
- replace with
unknown - type boundaries
- use generics
- use utility types
- add JSDoc
- write declaration files
- narrow types progressively
2. Progressive strictness¶
- start loose
- enable
checkJs - add strictness gradually
- finish with full
strictmode
This is exactly how real teams modernize large JavaScript codebases without breaking everything at once.