Clean Code in TypeScript: Principles and Practical Examples
\n\nEvery developer has encountered a situation where, a month after writing code, their own script seems foreign and incomprehensible. In the world of TypeScript, where strict typing provides an advantage, Clean Code principles become especially important. Clean code is not just about aesthetics; it's a tool for reducing bugs, speeding up development, and facilitating teamwork. In this article, we will break down the key principles of Clean Code in TypeScript with real examples that will help you write code that is easy to read, test, and maintain.
\n\n1. Naming: Say What the Variable Does
\n\nThe first thing a developer sees is the names of variables, functions, and classes. A bad name forces the reader to spend time deciphering it. In TypeScript with its type system, the name should be self-documenting.
\n\nBad example:
\nconst d = new Date();\nconst arr = [1, 2, 3];\nfunction proc(x: number, y: number) {\n return x * y;\n}\n\nGood example:
\nconst currentDate = new Date();\nconst userScores: number[] = [1, 2, 3];\nfunction calculateArea(width: number, height: number): number {\n return width * height;\n}\n\nUse nouns for variables and verbs for functions. Avoid abbreviations (except for commonly accepted ones like id or url). TypeScript allows you to add types, but the name should still reflect the essence.
2. Functions: Small, Clean, and Predictable
\n\nA function should do only one thing. If you can't come up with a short name for a function, it's likely doing too much. It's also important to avoid side effects.
\n\nSingle Responsibility Principle (SRP) at the function level:
\n\n// Bad: function does everything at once\nfunction processUserData(user: User): void {\n // Validation\n if (!user.email) throw new Error('Email required');\n // Save to DB\n database.save(user);\n // Send email\n emailService.sendWelcome(user.email);\n}\n\n// Good: separate responsibilities\nfunction validateUser(user: User): boolean {\n return user.email !== undefined;\n}\n\nfunction saveUser(user: User): void {\n database.save(user);\n}\n\nfunction sendWelcomeEmail(email: string): void {\n emailService.send(email);\n}\n\nfunction processUser(user: User): void {\n if (!validateUser(user)) {\n throw new Error('Invalid user data');\n }\n saveUser(user);\n sendWelcomeEmail(user.email);\n}\n\nAlso avoid a large number of parameters. If there are more than two, combine them into an object:
\n\n// Bad\nfunction createUser(name: string, email: string, age: number, isAdmin: boolean): void {}\n\n// Good\ninterface CreateUserParams {\n name: string;\n email: string;\n age: number;\n isAdmin: boolean;\n}\n\nfunction createUser(params: CreateUserParams): void {}\n\n3. Typing: Use TypeScript's Capabilities to the Fullest
\n\nOne of the main advantages of TypeScript is strict typing. But many developers use any as a crutch, killing all the benefits. Clean code in TypeScript is code where types are explicitly described, not guessed.
Avoid any:
// Bad\nfunction getData(): any {\n return fetch('/api/data');\n}\n\n// Good\ninterface ApiResponse {\n id: number;\n title: string;\n}\n\nasync function getData(): Promise<ApiResponse> {\n const response = await fetch('/api/data');\n return response.json();\n}\n\nUse discriminated unions for complex states:
\n// Bad: unclear what's in the object\ntype Status = 'loading' | 'success' | 'error';\n\ninterface State {\n status: Status;\n data?: Data;\n error?: string;\n}\n\n// Good: each