TypeScript is one of the best investments you can make in a JavaScript codebase. It catches bugs before production, improves IDE support, and makes large applications easier to maintain.
But here's the catch: TypeScript only helps when you use it correctly.
Over the years, I've seen developers (myself included) make the same mistakes repeatedly. These mistakes don't always produce compiler errors, they often create false confidence, hidden bugs, and hours of debugging.
Let's look at some of the most common ones.
1. Using any Everywhere
The fastest way to silence TypeScript is also the fastest way to lose its benefits.
❌ Bad
function processUser(user: any) {
return user.name.toUpperCase();
}
This compiles even if user.name doesn't exist.
✅ Better
interface User {
name: string;
}
function processUser(user: User) {
return user.name.toUpperCase();
}
Or, if the shape is unknown:
function processUser(user: unknown) {
if (
typeof user === "object" &&
user !== null &&
"name" in user
) {
return String(user.name).toUpperCase();
}
}
Rule: Use unknown instead of any whenever possible.
2. Ignoring strict Mode
Many projects still start with:
{
"strict": false
}
This disables many of TypeScript's most useful safety checks.
Enable:
{
"compilerOptions": {
"strict": true
}
}
You'll immediately catch issues like:
- undefined values
- missing properties
- incorrect function calls
- unsafe assignments
Yes, migration takes time, but the payoff is huge.
3. Overusing Type Assertions
Developers often force TypeScript to agree with them.
const user = response as User;
This tells TypeScript:
"Trust me."
Even when it shouldn't.
A safer approach is validating data.
if ("name" in response) {
console.log(response.name);
}
Or use runtime validation libraries like:
- Zod
- Valibot
- io-ts
Type assertions should be the exception, not the default.
4. Confusing interface and type
Many developers think they're interchangeable.
Not exactly.
Use interface for object contracts.
interface User {
id: number;
name: string;
}
Use type for unions, intersections, tuples, and utility types.
type Status =
| "loading"
| "success"
| "error";
A simple rule:
- Object shape →
interface - Everything else →
type
5. Forgetting to Narrow Union Types
Consider this example:
type Result =
| { success: true; data: string }
| { success: false; error: string };
function handle(result: Result) {
console.log(result.data);
}
TypeScript complains because data isn't always available.
Correct:
function handle(result: Result) {
if (result.success) {
console.log(result.data);
} else {
console.log(result.error);
}
}
Union types are powerful, but only when properly narrowed.
6. Making Everything Optional
This interface looks flexible.
interface User {
id?: number;
name?: string;
email?: string;
}
In reality, it's dangerous.
Now every property could be undefined.
Instead, make only genuinely optional fields optional.
interface User {
id: number;
name: string;
email?: string;
}
7. Using Object Instead of Proper Types
Avoid:
function print(data: Object) {}
or
let value: {};
These types tell TypeScript almost nothing.
Prefer:
Record<string, unknown>
or define a proper interface.
8. Not Typing API Responses
This is common:
const response = await fetch("/api/user");
const data = await response.json();
Now data becomes any.
Instead:
interface User {
id: number;
name: string;
}
const data: User = await response.json();
Even better, validate the response before trusting it.
Remember:
APIs can change.
Your types won't stop bad server responses.
9. Forgetting Generic Constraints
Without constraints:
function getId<T>(item: T) {
return item.id;
}
TypeScript correctly complains.
Use constraints:
function getId<T extends { id: number }>(item: T) {
return item.id;
}
Generics become much more useful when you define what they expect.
10. Fighting TypeScript Instead of Listening
Sometimes developers spend 20 minutes trying to silence an error.
Instead, ask:
Why is TypeScript complaining?
Often the compiler is pointing to:
- incorrect assumptions
- missing null checks
- inconsistent object shapes
- outdated interfaces
Many production bugs start as ignored TypeScript warnings.
Bonus Tip: Prefer Inference
You don't always need explicit types.
Instead of:
const name: string = "Alice";
Simply write:
const name = "Alice";
TypeScript already knows it's a string.
Use explicit annotations when they improve readability, not everywhere.
Final Thoughts
TypeScript isn't just a language, it's a safety net.
The more you work with it instead of around it, the more bugs it prevents before they ever reach production.
Quick Recap
✅ Avoid any
✅ Enable strict mode
✅ Don't abuse as
✅ Narrow union types
✅ Use interfaces and types appropriately
✅ Validate API responses
✅ Constrain generics
✅ Trust TypeScript's compiler
Conclusion
TypeScript is more than just a superset of JavaScript—it's a tool that helps you write reliable, maintainable, and scalable code. By avoiding these common mistakes, you'll spend less time debugging and more time building great applications.
Whether you're developing a small personal project or a large-scale web development application, following TypeScript best practices can significantly improve your code quality and developer experience.
Remember, TypeScript isn't there to slow you down—it's there to catch mistakes before your users do. Treat compiler errors as helpful feedback, not obstacles, and you'll build more robust software with greater confidence.
If you found this article helpful, share it with your fellow developers and let me know in the comments: Which TypeScript mistake has cost you the most time, and what lesson did you learn from it?
The goal isn't to make TypeScript happy.
The goal is to make your code safer, easier to refactor, and far easier to maintain.
What TypeScript mistake has cost you the most time? Share it in the comments, someone else will probably learn from it too.
Top comments (0)