clean-code-typescript
repository·main·Indexed 27 days ago
https://github.com/labs42io/clean-code-typescriptA collection of clean code principles adapted for TypeScript development. Based on Robert C. Martin's Clean Code, this guide provides guidelines for producing readable, reusable, and refactorable software, covering topics such as meaningful naming, function design, avoiding side effects, and leveraging TypeScript-specific features like enums, access modifiers, and generators.
What's inside clean-code-typescript
- clean-code-typescript is a guide for producing readable, reusable, and refactorable software in TypeScript. It adapts software engineering principles from Robert C. Martin's Clean Code specifically for TypeScript developers. It is intended as a set of guidelines to assess code quality rather than a strict style guide.
Organize import statements
mainFollow these rules to keep dependencies clear and readable:
- Alphabetize and Group: Alphabetize import statements and group them by type.
- Remove Unused: Delete unused imports.
- Alphabetize Named Imports: e.g.,
import { A, B, C } from 'foo';. - Alphabetize Sources: Alphabetize within groups (e.g.,
import * as a from 'a'; import * as b from 'b';). - Use
import type: Useimport typeinstead ofimportwhen only importing types to avoid runtime dependency cycles. - Delineate Groups: Use blank lines to separate groups.
Required Group Order:
- Polyfills (e.g.,
import 'reflect-metadata';) - Node builtin modules (e.g.,
import fs from 'fs';) - External modules (e.g.,
import { query } from 'itiriri';) - Internal modules (e.g.,
import { UserService } from 'src/services/userService';) - Modules from a parent directory (e.g.,
import foo from '../foo';) - Modules from the same or sibling directory (e.g.,
import bar from './bar';)
Apply the Single Responsibility Principle (SRP)
mainEnsure a class has only one reason to change by making it conceptually cohesive. Avoid 'jam-packing' a class with multiple functionalities. If a class handles multiple responsibilities (e.g., managing user settings AND verifying credentials), split them into separate classes. This minimizes the impact on dependent modules when a change is required.
// Good: Separate concerns into UserAuth and UserSettings class UserAuth { constructor(private readonly user: User) {} verifyCredentials() { /* ... */ } } class UserSettings { private readonly auth: UserAuth; constructor(private readonly user: User) { this.auth = new UserAuth(user); } changeSettings(settings: UserSettings) { if (this.auth.verifyCredentials()) { // ... } } }Remove duplicate code through abstraction
mainAvoid duplicating logic by creating abstractions (classes, union types, or common parent classes) that can handle different but related entities. However, be cautious: if two pieces of code live in different domains, duplication might be preferable to an abstraction that introduces unnecessary dependencies between modules.
type Employee = Developer | Manager; function showEmployeeList(employee: Employee[]) { employee.forEach((employee) => { const expectedSalary = employee.calculateExpectedSalary(); const experience = employee.getExperience(); const extra = employee.getExtraDetails(); const data = { expectedSalary, experience, extra, }; render(data); }); }Use descriptive function names
mainFunction names should explicitly state what they do to avoid ambiguity. Avoid generic names like
addToDateif the specific operation (like adding a month) is important for the caller to know.function addMonthToDate(date: Date, month: number): Date { // ... } const date = new Date(); addMonthToDate(date, 1);Use descriptive names for tests
mainTest names should reveal their intention so that when a test fails, the name provides the first indication of what went wrong.
describe('Calendar', () => { it('should handle leap year', () => { // ... }); it('should throw when format is invalid', () => { // ... }); });Use getters and setters for object encapsulation
mainInstead of accessing properties directly on objects, use TypeScript's
getandsetsyntax. This encapsulates internal representation, allows for easy validation during assignment, enables lazy loading, and provides a single place to add logging or error handling without changing every accessor in the codebase.class BankAccount { private accountBalance: number = 0; get balance(): number { return this.accountBalance; } set balance(value: number) { if (value < 0) { throw new Error('Cannot set negative balance.'); } this.accountBalance = value; } // ... } const account = new BankAccount(); account.balance = 100;Always throw or reject with Error objects
mainWhen using
throworPromise.reject, always use theErrortype. This ensures you get a stack trace and compatibility withtry/catch/finallyblocks. Avoid throwing strings or plain objects.function calculateTotal(items: Item[]): number { throw new Error('Not implemented.'); } async function get(): Promise<Item[]> { throw new Error('Not implemented.'); }Prefer immutability with readonly and const assertions
mainTo prevent unexpected mutations and follow functional programming patterns, use immutability tools in TypeScript:
readonlymodifier: Mark individual properties in aninterfaceorclassasreadonly.Readonly<T>utility type: Use this to mark all properties of a type asreadonly.ReadonlyArray<T>: Use this for arrays to prevent methods likepush()orfill(), while still allowing non-mutating methods likeconcat()orslice().as const(Const Assertions): Use this for literal values to make objects and arrays deeply read-only.readonlyin function arguments: Usereadonly string[]to ensure a function does not mutate its input array.
// read-only object const config = { hello: 'world' } as const; config.hello = 'world'; // error // read-only array const array = [ 1, 3, 5 ] as const; array[0] = 10; // error // You can return read-only objects function readonlyData(value: number) { return { value } as const; } const result = readonlyData(100); result.value = 200; // errorAutomate TypeScript formatting with ESLint
mainTo avoid arguments over subjective formatting, use automated tools. For TypeScript, ESLint is recommended for static analysis to improve readability and maintainability.
Available ready-to-use configurations:
eslint-config-airbnb-typescript: Airbnb style guide.eslint-plugin-base-style-config: Essential ESLint rules for JS, TS, and React.eslint-config-prettier: Lint rules for the Prettier code formatter.
If migrating from TSLint to ESLint, use the
tslint-to-eslint-configproject.Use iterators and generators for data streams
mainWhen working with collections that can be treated as a stream, use Generators (
function*) and Iterables. This allows for lazy execution, decoupling the consumer from the implementation, and efficient memory usage for large datasets.function* fibonacci(): IterableIterator<number> { let [a, b] = [0, 1]; while (true) { yield a; [a, b] = [b, a + b]; } } // Usage with a loop for (const fib of fibonacci()) { // ... }Apply the Interface Segregation Principle (ISP)
mainAvoid forcing clients to depend on interfaces they do not use. Instead of creating large, 'fat' interfaces that include methods a client might not need (forcing them to implement empty or error-throwing methods), split the interfaces into smaller, more specific ones. This allows classes to implement only the specific behaviors they actually support.