Skip to content

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 strict mode

This is exactly how real teams modernize large JavaScript codebases without breaking everything at once.