Replace any During TypeScript Migration
A systematic approach to finding and replacing any types during JavaScript-to-TypeScript migration. Learn strategies per context and how to tighten types over time.
You replace any TypeScript types by finding each one and swapping it for something the compiler can actually check: a specific type, unknown, or a generic. any is the escape hatch of TypeScript, and it disables type checking wherever it appears. During migration, any is a useful temporary tool, but keeping it permanently undermines the reason you adopted TypeScript in the first place.
Replacing any is a gradual process. You do not need to remove every any at once. Start with the ones that cause the most damage and work inward.
The decision flow leads to one of four outcomes: add a specific type, use a generic, switch to unknown, or temporarily keep any with a plan to revisit it.
Step 1: Find the any Types
Enable noImplicitAny in your tsconfig. This flags every place where TypeScript cannot infer a type and would silently fall back to any:
{
"compilerOptions": {
"noImplicitAny": true
}
}Run tsc against a project with this setting on, and every place TypeScript could not infer a type produces an error like these:
Parameter 'data' implicitly has an 'any' type.
Parameter 'callback' implicitly has an 'any' type.These are your targets. Each one is a place where TypeScript gave up and your code has no type safety.
For explicit any annotations that you wrote intentionally, search your codebase:
grep -r ": any" src/
grep -r "as any" src/For more on the compiler option, see NoImplicitAny in TypeScript.
Step 2: Replace With Specific Types
The most common case: you used any during migration because you did not know the type yet, but now you do.
// Before (migration escape hatch)
function processOrder(order: any) {
console.log(order.id, order.total);
}Read how the function uses the parameter. Every property access tells you what type it needs. If the id property is logged and total is used in arithmetic, the type emerges from the usage:
// After (you now know the shape)
interface Order {
id: string;
total: number;
items: string[];
}
function processOrder(order: Order) {
console.log(order.id, order.total);
}The same reading technique works for return types. Look at what the function actually returns, not just what it accepts, and pay attention to any parsing or transformation happening inside the body:
// Before
function parseResponse(response: any): any {
return JSON.parse(response.body);
}The parameter only needs a body field, and the return value has a known shape once you look at how callers use the parsed result:
// After
interface ApiBody {
results: string[];
count: number;
}
function parseResponse(response: { body: string }): ApiBody {
return JSON.parse(response.body);
}If the function returns the result of another typed function, let TypeScript infer the return type and remove the explicit any.
Step 3: Replace With unknown
When you genuinely do not know the type of a value, use unknown instead of any:
// Before
function handleMessage(message: any) {
console.log(message.text);
}
// After
function handleMessage(message: unknown) {
if (typeof message === "object" && message !== null && "text" in message) {
console.log((message as { text: string }).text);
}
}unknown preserves the uncertainty while forcing you to check the type before using the value. This is the single most impactful replacement you can make. Every any you replace with unknown eliminates a source of hidden bugs.
For more on working with unknown, see Handle Dynamic Values in TypeScript.
Step 4: Replace With Generics
When any appears because you need a relationship between types, where the input type determines the output type, use a generic instead:
// Before
function firstItem(list: any): any {
return list[0];
}
// After
function firstItem<T>(list: T[]): T {
return list[0];
}The generic T captures the relationship: if you pass an array of strings, you get back a string. If you pass an array of User objects, you get back a User.
Another common pattern is a function that wraps another function, such as adding logging around a call. Typing both the input and output as any throws away everything the caller knew about the wrapped function:
// Before
function withLogging(fn: any): any {
return (...args: any[]) => {
console.log("Calling", fn.name);
return fn(...args);
};
}A generic constrained to function types preserves the original parameter and return types all the way through the wrapper, so callers get full type safety on the result:
// After: preserve the original function's type
function withLogging<T extends (...args: unknown[]) => unknown>(fn: T): T {
return ((...args: unknown[]) => {
console.log("Calling", fn.name);
return fn(...args);
}) as T;
}For more on generics, see Generic Functions in TypeScript.
Step 5: Replace any in Specific Contexts
Array of any. An array typed as any[] allows any mix of values with no checking on individual elements. Replace it with a specific element type when you know it, or unknown[] when you genuinely do not:
// Before
let items: any[] = [];
// After: you know the items are strings
let items: string[] = [];
// After: you genuinely do not know
let items: unknown[] = [];Callback any. A handler typed simply as any gives no feedback if a caller passes the wrong kind of function. Write out the callback's own parameter and return types instead:
// Before
function on(event: string, handler: any): void { /* ... */ }
// After
function on(event: string, handler: (...args: unknown[]) => void): void { /* ... */ }Object any. An index signature typed as { [key: string]: any } accepts anything at any key, so typos and wrong value types pass silently. Replace it with a specific interface when the keys are known, or a generic Record when they are not:
// Before
const cache: { [key: string]: any } = {};
// After: you know the cache stores User objects
const cache: { [key: string]: User } = {};
// After: multiple possible value types
const cache: Record<string, unknown> = {};any as a type assertion. Casting through any silences the compiler completely, even when the real type is easy to name. Replace an as-any cast with a more precise assertion or a runtime type guard:
// Before
const button = document.getElementById("submit") as any;
// After
const button = document.getElementById("submit") as HTMLButtonElement | null;
if (button) {
button.click();
}When to Keep any (Temporarily)
Some situations make any hard to replace immediately. In these cases, keep any but mark it:
- Third-party libraries without types. Create a declaration file instead of using any at every call site. See Declaration Files in TypeScript.
- Extremely dynamic code. Code that constructs object paths at runtime or uses eval-like patterns is genuinely hard to type. Use any as a stopgap and plan a refactor.
- Migration in progress. When you are actively converting a file and need it to compile before all types are ready, any is acceptable as a temporary placeholder.
Add a comment explaining why any is there and when you plan to replace it:
// MIGRATION: Replace any with UserPreferences after preferences module is typed
function loadPreferences(userId: string): any {
return legacyPreferencesStore.get(userId);
}A lint rule like @typescript-eslint/no-explicit-any set to warn can prevent new any types from being added while you clean up existing ones.
Prioritize by Impact
Replace any in this order for maximum benefit:
| Priority | Where | Why |
|---|---|---|
| 1 | Public API functions | Every caller benefits from correct types |
| 2 | Shared utility functions | Used across the codebase, high leverage |
| 3 | Data models and types | Defines the shapes the rest of the code trusts |
| 4 | Service and API layers | Boundary between your code and external data |
| 5 | Internal helper functions | Lower impact, clean up last |
Start at the edges of your module graph and work inward. Each any you remove at a module boundary improves type safety for every file that imports from that module.
Rune AI
Key Insights
- Enable noImplicitAny to find places where TypeScript infers any.
- Replace any with unknown for values whose type you genuinely cannot know.
- Replace any with specific types derived from the function body or usage.
- Use generics to preserve type relationships instead of falling back to any.
- Temporarily keep any in migration edge cases, but track them with a lint rule.
- Focus replacements on public API surfaces and core business logic first.
Frequently Asked Questions
Is it realistic to remove every any from a codebase?
What is the safest replacement for any?
How do I find all any usages in my project?
Conclusion
Replacing any is a gradual process. Start by enabling noImplicitAny to find inferred any types. Replace explicit any with unknown, specific types, or generics depending on the context. Focus on public API surfaces first, and leave internal code for later. Every any you remove improves type safety for the rest of the codebase.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.