-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
249: Practice Exercise: Refactoring an any-Heavy Codebase to Strict Types
I recently inherited a project where the previous developer seemed to treat any as a shortcut for "I don't feel like thinking about this right now." It's a common trap. You're moving fast, the types are getting complex, and suddenly your codebase is just JavaScript with .ts extensions. It's dangerous because you lose the very reason we're using TypeScript in the first place: the safety net.
Let's look at this snippet I found in the project. It's a basic user settings handler. I'll share the code, and then we'll try to fix it together.
function updateUserSettings(user: any, settings: any) {
const updatedUser = { ...user, ...settings };
if (updatedUser.role === 'admin') {
console.log("Updating admin permissions...");
}
return updatedUser;
}
const currentUser = { id: 1, name: 'Alice', role: 'user' };
const newSettings = { theme: 'dark', role: 'admin' };
const result = updateUserSettings(currentUser, newSettings);
console.log(result.userName); // Wait, is it 'name' or 'userName'?
Tugging on the first thread
At first glance, this works. But look at that last line: result.userName. The code doesn't crash immediately, but it returns undefined because the property is actually name. If this were a larger app, that bug could propagate through five different components before you noticed it. That's the "any" tax.
My first instinct is to just slap an interface on user. Let's try that.
interface User {
id: number;
name: string;
role: 'admin' | 'user';
}
function updateUserSettings(user: User, settings: any) {
const updatedUser = { ...user, ...settings };
// ...
return updatedUser;
}
I thought I fixed it, but the return type of updateUserSettings is now inferred as User & { [x: string]: any }. Because settings is still any, TypeScript allows me to merge anything into the user object. I can still call result.userName or result.favoritePizza and the compiler will just shrug its shoulders. We've moved the any, but we haven't killed it.
Dealing with the ripple effect
Now I realize the problem is the settings parameter. But wait—settings aren't always a full User object. Sometimes we only update the theme, or just the role. I can't just make settings a User because then the caller would be forced to provide an id and name every time they want to change a theme.
This is where I usually stop and think about Partial. If settings is a partial version of User, we get the benefit of type checking without the rigidity of requiring every field.
function updateUserSettings(user: User, settings: Partial<User>) {
const updatedUser = { ...user, ...settings };
if (updatedUser.role === 'admin') {
console.log("Updating admin permissions...");
}
return updatedUser;
}
This is better. If I try to pass { role: 'super-admin' }, TypeScript will scream at me because 'super-admin' isn't part of the 'admin' | 'user' union. We've regained control over the role field. But there's still a gap: the theme property. My original newSettings had a theme, but my User interface doesn't. The compiler is now flagging theme as an error.
Hardening the boundaries
I've discovered that my User interface was too narrow. It didn't actually represent the data we were working with. In a real refactor, this is the "discovery phase." The any was hiding the fact that our data model was incomplete.
Let's define a proper UserSettings type and combine them. I'll use an intersection type here because a user has settings.
interface User {
id: number;
name: string;
role: 'admin' | 'user';
}
interface UserSettings {
theme: 'light' | 'dark';
notificationsEnabled: boolean;
}
type CompleteUser = User & UserSettings;
function updateUserSettings(user: CompleteUser, settings: Partial<CompleteUser>): CompleteUser {
return { ...user, ...settings };
}
const currentUser: CompleteUser = {
id: 1,
name: 'Alice',
role: 'user',
theme: 'light',
notificationsEnabled: true
};
const newSettings: Partial<CompleteUser> = { theme: 'dark', role: 'admin' };
const result = updateUserSettings(currentUser, newSettings);
// result.userName -> Error: Property 'userName' does not exist on type 'CompleteUser'.
// result.name -> Perfect.
Now the code is honest. The types tell the truth about what the data is and what it can be. The process wasn't just "replacing any with a name"; it was about observing where the compiler failed and using those failures to map out the actual shape of the data. That's how you refactor a codebase without breaking it.
📋 Practical Task
Exercise: Refactoring the Legacy Order Pipeline
You've been handed a legacy order processing module. It's riddled with any types, and bugs are slipping through because the system doesn't know if an order is "Pending," "Shipped," or "Cancelled."
Your Task: Refactor the following code to remove all any types. You must:
- Create a
OrderStatusunion type (Pending, Shipped, Delivered, Cancelled). - Create an
Orderinterface that includes anid(number),customerName(string), andstatus(OrderStatus). - Ensure the
updateOrderStatusfunction usesPartial<Order>for its updates to allow changing only the status without providing the whole order object. - Fix the bug where the code tries to access
order.deliveryDateon an order that might not be "Delivered". (Hint: Use a type guard or a conditional check to ensure the property exists).
// LEGACY CODE TO REFACTOR
function updateOrderStatus(order: any, updates: any) {
return { ...order, ...updates };
}
function printShippingLabel(order: any) {
console.log(`Shipping to ${order.custName} via ${order.carrier}`);
}
const myOrder = { id: 101, customerName: 'Bob', status: 'Pending' };
const update = { status: 'Shipped', carrier: 'FedEx' };
const updatedOrder = updateOrderStatus(myOrder, update);
printShippingLabel(updatedOrder);
There are no comments for now.