40.2 Avoid Overusing any: Strategies for Incremental Typing
Look, we’ve all been there. You’re wrestling with a piece of JavaScript that’s more tangled than last year’s Christmas lights. The deadline is looming, and the siren song of any is calling: “Just one little any and all this pain will go away.” I get it. But using any is like putting a “Bugs Here!” sign on your code. It defeats the entire purpose of using TypeScript. You’re just writing verbose JavaScript at that point.
The goal isn’t to eliminate any instantly; that’s a recipe for frustration. The goal is to use it as a temporary scaffold, not the foundation. We’re going to talk about how to progressively replace that scaffold with something solid, a process often called incremental typing.
Start with unknown Before You Reach for any
The main problem with any is that it’s both a way out of the type system and a way in. It’s contagious. If you assign an any to a string, that string is now infected and also treated as any in many contexts. It’s the zombie apocalypse of type safety.
Enter unknown. It’s the type-safe counterpart to any. You can assign anything to an unknown variable, but the TypeScript compiler won’t let you do anything with it until you prove what it is. It forces you to perform a type check, making you deal with the uncertainty right then and there.
// With `any` - a disaster waiting to happen
function dangerousFunction(data: any) {
console.log(data.name.toUpperCase()); // Runtime error if data is null? Tough luck.
data(); // Sure, why not, let's call it! What could go wrong?
}
// With `unknown` - the compiler makes you check your work
function safeFunction(data: unknown) {
// console.log(data.name.toUpperCase()); // Error: Object is of type 'unknown'
// data(); // Error: Object is of type 'unknown'
// First, prove it's an object and has a `name` property.
if (typeof data === 'object' && data !== null && 'name' in data) {
// Now TypeScript knows! We've narrowed the type.
console.log((data as { name: string }).name.toUpperCase()); // We can use an assertion here
}
// Or, even better, use a type guard
if (isPerson(data)) {
console.log(data.name.toUpperCase()); // No assertion needed
}
}
// A custom type guard
function isPerson(obj: unknown): obj is { name: string } {
return typeof obj === 'object' && obj !== null && 'name' in obj;
}
Use unknown for data whose shape you truly don’t know yet, like JSON parsed from an external API. It’s your first and best line of defense.
Leverage Type Assertions (Carefully!)
Sometimes, you just know more than the compiler. Maybe you’re initializing a mock object for a test, or you’ve received data from a legacy API that you can trust. This is where type assertions come in. They’re you telling the compiler, “I’ve got this, trust me.” Use them sparingly, because when you’re wrong, the compiler will not be there to save you.
The key is to use the as syntax, not the old angle brackets (<Type>), which conflict with JSX.
// A quick and dirty mock for a test? Okay, fine.
const mockUser = {
id: 123,
name: 'Test User',
// ...oops, forgot `email` but the test doesn't need it
} as User;
// Parsing JSON from a known, trusted source? Assert away.
const config: AppConfig = JSON.parse(configString) as AppConfig;
// DANGER ZONE: This will silence the compiler, but explode at runtime.
const badAssertion = {} as User;
console.log(badAssertion.name.toUpperCase()); // Runtime Error: Cannot read properties of undefined
The rule of thumb: only assert to a type that is a subset of the possible types. Empty object {} is not a subset of User, so that assertion is a lie. The mock user example is a “white lie” – it’s mostly shaped like a User, so the risk is low.
Define Interfaces for External Data
You will constantly be dealing with data from the outside world: API responses, configuration files, user input. This data is the wild west, and assuming it conforms to your perfect internal types is a classic rookie mistake.
The solution is to define separate interfaces that describe the actual, messy reality of the external data. I often prefix these with API or Raw.
// What we get from the API - the harsh reality
interface APIUser {
Id: number; // Yep, the API uses PascalCase... cool, cool.
FullName: string;
last_login: string; // And sometimes snake_case. Of course.
address?: any; // This is just a junk drawer of data.
}
// What we want in our frontend - the pristine ideal
interface User {
id: number;
name: string;
lastLogin: Date; // A proper Date object!
}
// Your job is to write a function to sanitize the wild west into civilized types
function transformUser(apiUser: APIUser): User {
return {
id: apiUser.Id,
name: apiUser.FullName,
lastLogin: new Date(apiUser.last_login), // Transformation happens here
};
// We consciously ignore the `address` junk drawer because we don't need it.
}
This pattern is invaluable. It creates a clear boundary between the untrusted outside and your trusted application core. You handle all the weirdness in one obvious place, the transformation function, instead of letting it leak everywhere.
Use @ts-expect-error and @ts-ignore as Temporary Annotations
So you’ve inherited a giant codebase and there’s one line that’s throwing a type error you can’t fix right now. You need to deploy. Reaching for any is the nuclear option. Instead, use the surgical scalpel: @ts-expect-error or @ts-ignore.
The difference is important:
@ts-ignoreunconditionally silences the next line’s error.@ts-expect-errorsilences the next line’s error, but will itself throw an error if the next line doesn’t actually have an error. This is brilliant because it acts as a TODO marker. Once you fix the underlying issue, this comment will start causing an error, reminding you to delete it.
// legacyJavascriptLib is a thing that exists and we can't type it yet.
// @ts-expect-error: This library isn't typed yet, we need to fix this in Q2.
const result = legacyJavascriptLib.obscureFunction();
// After we eventually add types for obscureFunction and fix the error...
// The @ts-expect-error comment will now have a red squiggly line underneath it!
// It says "Unused '@ts-expect-error' directive." – a perfect reminder to clean it up.
Treat these comments like a “Wet Floor” sign. You don’t leave them there forever; you put them down because there’s an immediate danger, and you remove them the moment the floor is dry.