3 Years of TypeScript: Practical Tips for Clean Code

Recently, I was refactoring a project from two years ago, and when I opened the code, I almost wanted to delete the repo and run away. Variable names like data1
, data2
, functions with 100+ lines, types all set to any
... it was a disaster scene.
After some painful reflection, I've summarized some practical tips for writing cleaner TypeScript code. These are all lessons learned from stepping on landmines in real projects, and I hope they can help you avoid writing "spaghetti code."
1. Give Good Names, Don't Make People Guess
This is really crucial. Good variable names are the best comments.
My previous garbage code:
// What the hell is this?
function calculate(a: number, b: number, c: number) {
return a + b * c;
}
How I write it now:
function calculateOrderTotal(price: number, quantity: number, taxRate: number): number {
const subtotal = price * quantity;
const taxAmount = subtotal * taxRate;
return subtotal + taxAmount;
}
See the difference? The second version is self-explanatory without comments.
Here's a small tip: For TypeScript interface naming, I now use User
directly instead of IUser
. The modern approach is cleaner.
2. One Function, One Job
This principle sounds simple, but it's easy to violate in real projects. I used to write these "god functions" all the time:
Bad example:
function fetchAndProcessUserData(userId: string) {
// 1. Fetch data
const response = fetch(`/api/users/${userId}`);
// 2. Parse data
const user = response.json();
// 3. Validate data
if (!user.name || !user.email) {
throw new Error("Invalid user data");
}
// 4. Save to database
db.save(user);
}
This function does 4 things! Debugging was a nightmare.
After improvement:
async function fetchUserData(userId: string): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
function validateUserData(user: User): boolean {
return !!(user.name && user.email);
}
async function saveUser(user: User): Promise<void> {
await db.save(user);
}
async function handleUserRequest(userId: string): Promise<void> {
const user = await fetchUserData(userId);
if (validateUserData(user)) {
await saveUser(user);
}
}
Now each function has a clear responsibility, making testing and maintenance much easier.
3. Stop Using any
, Leverage TypeScript's Type System Properly
TypeScript's type system is its biggest advantage, but I've found many people (including my past self) often get lazy and use any
.
Use Specific Types, Not any
any
is like opening a backdoor in the type system. Use it too much and you lose the point of TypeScript.
Make Good Use of Union Types and Literal Types
// This is too broad
type Status = string;
// This is more precise, and IDE can auto-complete
type OrderStatus = "pending" | "processing" | "shipped" | "delivered" | "cancelled";
function handleOrderStatus(status: OrderStatus) {
// TypeScript will check if you've handled all possible states
switch (status) {
case "pending":
// ...
break;
case "processing":
// ...
break;
// If you miss a state, TypeScript will remind you
}
}
Define Clear Data Structures
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
interface Product {
id: string;
name: string;
price: number;
}
By defining data models this way, the entire team knows what the data looks like.
4. Handle Errors Gracefully
I used to return null
or error codes frequently, but now I prefer throwing exceptions. TypeScript can help us define custom error types:
class NetworkError extends Error {
constructor(message: string) {
super(message);
this.name = "NetworkError";
}
}
class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
}
async function fetchData(): Promise<Data> {
try {
const response = await fetch("/api/data");
if (!response.ok) {
throw new NetworkError("Failed to fetch data");
}
return response.json();
} catch (error) {
if (error instanceof NetworkError) {
// Handle network error
}
throw error;
}
}
5. Write Testable Code
I have deep experience with this. My old code was extremely painful to test, but after learning dependency injection, the world became much cleaner.
Hard-to-test code:
class UserService {
private db = new DatabaseClient(); // Direct dependency on concrete implementation
async getUser(id: number): Promise<User> {
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
Testing this code requires connecting to a real database, which is too troublesome.
Testable code:
interface IDatabase {
query(sql: string): Promise<any>;
}
class UserService {
constructor(private db: IDatabase) {} // Depends on abstract interface
async getUser(id: number): Promise<User> {
return this.db.query(`SELECT * FROM users WHERE id = ${id}`);
}
}
// Easy to mock during testing
const mockDb: IDatabase = {
query: async (sql) => ({ id: 1, name: "Test User" }),
};
const userService = new UserService(mockDb);
Summary
After writing code for so many years, I've found that the most important thing isn't showing off skills, but making code easy to understand and maintain. TypeScript gives us great tools; the key is to use them well.
Remember these key points:
- Give good names, let code speak for itself
- Keep functions single-purpose for easier testing and maintenance
- Fully leverage the type system, don't get lazy with
any
- Handle errors gracefully, define clear exception types
- Make code testable through dependency injection
These tips seem simple, but sticking to them in real projects isn't easy. However, once you develop the habit, you'll find a qualitative improvement in code quality.
Final thought: Code is written for humans to read; machines just happen to execute it. Invest time in making code clearer, and your future self will thank your present self.
If this article helped you, feel free to share it with more developer friends. Let's write better code together!
Follow WeChat Official Account

Scan to get:
- • Latest tech articles
- • Exclusive dev insights
- • Useful tools & resources
💬 评论讨论
欢迎对《3 Years of TypeScript: Practical Tips for Clean Code》发表评论,分享你的想法和经验