Clean Code in TypeScript: How to Write Code You Won't Be Ashamed to Show
Clean Code is not just a trendy term, but a fundamental approach to development that turns a chaotic set of instructions into an understandable and maintainable system. For TypeScript, as a statically typed language, the principles of clean code take on special significance: strict typing and powerful compiler tools allow you to catch errors at the development stage, but only if the code is structured correctly. In this article, we will break down the key principles of Clean Code as applied to TypeScript, backing them up with concrete examples.
1. Naming Variables, Functions, and Classes
A variable or function name should answer the question "what is this?" or "what does it do?". In TypeScript, thanks to typing, names become even more informative. Avoid single-letter names (except for loop indices) and abbreviations that only you understand.
// Badconst d = new Date();const fn = (a: number, b: number) => a + b;
// Goodconst currentDate = new Date();const sum = (firstNumber: number, secondNumber: number): number => firstNumber + secondNumber;Use nouns for classes, and verbs or verb phrases for functions. Boolean variables should start with is, has, should.
// Badconst flag = true;function validate() { /* ... */ }
// Goodconst isUserActive = true;function validateEmail(email: string): boolean { /* ... */ }2. Single Responsibility Principle (SRP) for Functions
Each function should do exactly one thing and do it well. If a function performs multiple actions, break it down into smaller ones. TypeScript allows you to strictly type input and output data, which makes refactoring easier.
// Bad: the function finds a user, checks the password, and sends an emailasync function loginUser(email: string, password: string): Promise<void> { const user = await findUserByEmail(email); if (user && user.password === password) { await sendLoginEmail(user.email); console.log('Login successful'); }}
// Good: separate responsibilitiesasync function findUserByEmail(email: string): Promise<User | null> { /* ... */ }function isPasswordValid(inputPassword: string, storedHash: string): boolean { /* ... */ }async function notifyUserLogin(email: string): Promise<void> { /* ... */ }
async function loginUser(email: string, password: string): Promise<boolean> { const user = await findUserByEmail(email); if (!user || !isPasswordValid(password, user.passwordHash)) { return false; } await notifyUserLogin(user.email); return true;}3. Using Types and Interfaces Instead of any
One of the main features of TypeScript is its type system. Avoid any at all costs. If the type is not known in advance, use unknown, generics, or union types. This makes the code self-documenting and protects against errors.
// Badfunction getData(id: any): any { return fetch(`/api/${id}`);}
// Goodinterface ApiResponse<T> { data: T; error?: string;}
async function getData<T>(id: string): Promise<ApiResponse<T>> { const response = await fetch(`/api/${id}`); return response.json();}Use strict interfaces to describe the structure of objects. This simplifies refactoring and helps the IDE suggest properties.
interface User { readonly id: number; name: string; email: string; role: 'admin' | 'user';}
function updateUserName(user: User, newName: string): User { return { ...user, name: newName };}