- 🛠️ **Refactor**: Updated return search result component for mobile responsiveness - 🗑️ **Chore**: Removed unused order-by dropdown component and related files - 📚 **Docs**: Enhanced component documentation for clarity
16 KiB
Code Style Guidelines
General Principles
- Readability First: Write code that is easy to read and understand.
- Consistency: Follow the same patterns and conventions throughout the codebase.
- Clean Code: Avoid unnecessary complexity and keep functions small and focused.
- SOLID Principles: Follow SOLID design principles to create more maintainable, flexible, and scalable code.
SOLID Design Principles
SOLID is an acronym for five design principles that help make software designs more understandable, flexible, and maintainable:
-
Single Responsibility Principle (SRP): A class should have only one reason to change, meaning it should have only one job or responsibility.
// Good - Each class has a single responsibility class UserAuthentication { authenticate(username: string, password: string): boolean { // Authentication logic } } class UserRepository { findById(id: string): User { // Database access logic } } // Bad - Class has multiple responsibilities class UserManager { authenticate(username: string, password: string): boolean { // Authentication logic } findById(id: string): User { // Database access logic } sendEmail(user: User, message: string): void { // Email sending logic } } -
Open/Closed Principle (OCP): Software entities should be open for extension but closed for modification.
// Good - Open for extension interface PaymentProcessor { processPayment(amount: number): void; } class CreditCardProcessor implements PaymentProcessor { processPayment(amount: number): void { // Credit card processing logic } } class PayPalProcessor implements PaymentProcessor { processPayment(amount: number): void { // PayPal processing logic } } // New payment methods can be added without modifying existing code -
Liskov Substitution Principle (LSP): Objects of a superclass should be replaceable with objects of subclasses without affecting the correctness of the program.
// Good - Derived classes can substitute base class class Rectangle { constructor( protected width: number, protected height: number, ) {} setWidth(width: number): void { this.width = width; } setHeight(height: number): void { this.height = height; } getArea(): number { return this.width * this.height; } } class Square extends Rectangle { constructor(size: number) { super(size, size); } // Preserve behavior when overriding methods setWidth(width: number): void { super.setWidth(width); super.setHeight(width); } setHeight(height: number): void { super.setWidth(height); super.setHeight(height); } } -
Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use.
// Good - Segregated interfaces interface Printable { print(): void; } interface Scannable { scan(): void; } class AllInOnePrinter implements Printable, Scannable { print(): void { // Printing logic } scan(): void { // Scanning logic } } class BasicPrinter implements Printable { print(): void { // Printing logic } } // Bad - Fat interface interface OfficeMachine { print(): void; scan(): void; fax(): void; staple(): void; } // Classes must implement methods they don't need -
Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules. Both should depend on abstractions.
// Good - Depending on abstractions interface Logger { log(message: string): void; } class ConsoleLogger implements Logger { log(message: string): void { console.log(message); } } class FileLogger implements Logger { log(message: string): void { // File logging logic } } class UserService { constructor(private logger: Logger) {} createUser(user: User): void { // Create user logic this.logger.log(`User created: ${user.name}`); } } // The UserService depends on the abstraction (Logger interface) // not on concrete implementations
Following these principles improves code quality by:
- Reducing coupling between components
- Making the system more modular and easier to maintain
- Facilitating testing and extension
- Promoting code reuse
Extended Guidelines for Angular and TypeScript
This section extends the core code style principles with Angular-specific and advanced TypeScript best practices to ensure consistency and maintainability in our projects.
Angular Enhancements
- Change Detection: Use the OnPush strategy by default for better performance.
- Lifecycle Hooks: Explicitly implement Angular lifecycle interfaces (OnInit, OnDestroy, etc.).
- Template Management: Keep templates concise and use the async pipe to handle observables.
- Component Structure: Follow best practices for component modularization to enhance readability and testability.
- Naming Conventions: Follow Angular's official naming conventions for selectors, files, and component classes.
- File Organization: Structure files according to features and follow the recommended folder structure.
- Control Flow: Use modern control flow syntax (@if, @for) instead of structural directives (*ngIf, *ngFor).
- Signals: Prefer signals over RxJS for simpler state management within components.
TypeScript Enhancements
- Strict Type Checking: Enable strict mode (
strict: true) and avoid excessive use ofany. - Interfaces vs. Types: Prefer interfaces for object definitions and use type aliases for unions and intersections.
- Generics: Use meaningful type parameter names and constrain generics when applicable.
- Documentation: Employ JSDoc comments for functions and generic parameters to improve code clarity.
- Non-Nullability: Use the non-null assertion operator (!) sparingly and only when you're certain a value cannot be null.
- Type Guards: Implement custom type guards to handle type narrowing safely.
- Immutability: Favor immutable data structures and use readonly modifiers when applicable.
- Exhaustiveness Checking: Use exhaustiveness checking for switch statements handling union types.
TypeScript Guidelines
-
Strict Typing:
- Enable
strict: truein tsconfig.json - Avoid
anyunless absolutely necessary - Use
unknowninstead ofanywhen type is truly unknown - Always specify return types for functions
- Use type inference for variable declarations where types are obvious
// Good const user: User = getUserById('123'); const items = ['apple', 'banana']; // Type inference is fine here // Bad const user: any = getUserById('123'); const items: string[] = ['apple', 'banana']; // Unnecessary type annotation - Enable
-
Interfaces and Types:
- Prefer
interfaceovertypefor object definitions - Use
typefor unions, intersections, and mapped types - Follow Angular's naming convention: Don't prefix interfaces with 'I' (use
ComponentPropsnotIComponentProps) - Extend interfaces instead of repeating properties
- Use readonly modifiers where appropriate
// Good interface BaseProps { readonly id: string; name: string; } interface UserProps extends BaseProps { email: string; } type ValidationResult = 'success' | 'error' | 'pending'; // Bad type UserProps = { id: string; name: string; email: string; }; - Prefer
-
Enums and Constants:
-
Prefer this order of implementation (from most to least preferred):
const enumfor better compile-time performance- Object literals with
as constfor runtime flexibility - Regular
enumonly when necessary for runtime access
-
When to use each approach:
- Use
const enumfor internal application enumerations that don't need runtime access - Use
const object as constwhen values need to be inspected at runtime or exported in an API - Use regular
enumonly when runtime enumeration object access is required
- Use
// Good - const enum (preferred for most cases) // Advantages: Tree-shakable, type-safe, disappears at compile time export const enum ConstEnumStates { NotSet = 'not-set', Success = 'success', } // Good - const object with 'as const' assertion // Advantages: Runtime accessible, works well with API boundaries export const ConstStates = { NotSet: 'not-set', Success: 'success', } as const; // Types can be extracted from const objects type ConstStatesType = (typeof ConstStates)[keyof typeof ConstStates]; // Least preferred - regular enum // Only use when you need the enum object at runtime export enum States { NotSet = 'not-set', Success = 'success', }- Use union types as an alternative for simple string literals
// Alternative approach using union types export type StatusType = 'not-set' | 'success'; -
-
Functions and Methods:
- Use arrow functions for callbacks and class methods
- Explicitly type parameters and return values
- Keep functions pure when possible
- Use function overloads for complex type scenarios
- Document complex functions with JSDoc
// Good /** * Fetches a user by ID and transforms it to the required format * @param id - The user's unique identifier * @param includeDetails - Whether to include additional user details */ const getUser = (id: string, includeDetails = false): Promise<User> => { // ...implementation }; // Bad function getUser(id) { // ...implementation } -
Generics:
- Use meaningful type parameter names (e.g.,
Tfor type,Kfor key) - Constrain generic types when possible using
extends - Document generic parameters using JSDoc
- Use meaningful type parameter names (e.g.,
Example:
// Good
interface UserProps {
id: string;
name: string;
}
interface AdminProps extends UserProps {
permissions: string[];
}
const enum UserRole {
Admin = 'ADMIN',
User = 'USER',
}
const getUser = <T extends UserProps>(id: string): Promise<T> => {
// ...implementation
};
// Bad
type User = {
id: any;
name: any;
};
function getUser(id) {
// ...implementation
}
Angular-Specific Guidelines
-
Components:
- Use OnPush change detection strategy by default
- Implement lifecycle hooks interfaces explicitly
- Keep templates small and focused
- Use async pipe instead of manual subscription management
// Good @Component({ selector: 'app-user-list', changeDetection: ChangeDetectionStrategy.OnPush, }) export class UserListComponent implements OnInit, OnDestroy { users$ = this.userService.getUsers().pipe(shareReplay(1)); } // Bad @Component({ selector: 'app-user-list', }) export class UserListComponent { users: User[] = []; subscription: Subscription; ngOnInit() { this.subscription = this.userService .getUsers() .subscribe((users) => (this.users = users)); } } -
Templates and Control Flow:
- Use modern control flow syntax (
@if,@for,@switch) instead of structural directives (*ngIf,*ngFor,*ngSwitch).
<!-- Good - Modern control flow syntax --> <div> @if (user) { <h1>Welcome, {{ user.name }}!</h1> } @else if (isLoading) { <h1>Loading user data...</h1> } @else { <h1>Please log in</h1> } <ul> @for (item of items; track item.id) { <li>{{ item.name }}</li> } @empty { <li>No items available</li> } </ul> @switch (userRole) { @case ('admin') { <app-admin-dashboard /> } @case ('manager') { <app-manager-dashboard /> } @default { <app-user-dashboard /> } } </div> <!-- Bad - Old structural directives --> <div> <h1 *ngIf="user">Welcome, {{ user.name }}!</h1> <h1 *ngIf="!user && isLoading">Loading user data...</h1> <h1 *ngIf="!user && !isLoading">Please log in</h1> <ul> <li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li> <li *ngIf="!items || items.length === 0">No items available</li> </ul> <app-admin-dashboard *ngIf="userRole === 'admin'"></app-admin-dashboard> <app-manager-dashboard *ngIf="userRole === 'manager'" ></app-manager-dashboard> <app-user-dashboard *ngIf="userRole !== 'admin' && userRole !== 'manager'" ></app-user-dashboard> </div>- When using
@for, always specify thetrackexpression to optimize rendering performance:- Use a unique identifier property (like
idoruuid) when available - Only use
$indexfor static collections that never change - Avoid using non-unique properties that could result in DOM mismatches
- Use a unique identifier property (like
- Leverage contextual variables in
@forblocks:$index- Current item index$first- Boolean indicating if this is the first item$last- Boolean indicating if this is the last item$even- Boolean indicating if this index is even$odd- Boolean indicating if this index is odd$count- Total number of items in the collection
<!-- Good - Using contextual variables --> @for (item of items; track item.id; let i = $index, isLast = $last) { <li [class.last-item]="isLast">{{ i + 1 }}. {{ item.name }}</li> }- Use the
@emptyblock with@forto handle empty collections gracefully - Store conditional expression results in variables for clearer templates:
<!-- Good - Storing expression result in variable --> @if (user.permissions.canEditSettings; as canEdit) { <button [disabled]="!canEdit">Edit Settings</button> } - Use modern control flow syntax (
Project-Specific Preferences
-
Frameworks: Follow best practices for Nx, Angular, date-fns, Ngrx, RxJs and Zod.
-
Testing: Use Jest with Spectator for unit tests and follow the Arrange-Act-Assert pattern.
-
File Naming:
-
Use kebab-case for filenames (e.g.,
my-component.ts). -
Follow a pattern that describes the symbol's feature then its type:
feature.type.ts// Good examples user.service.ts auth.guard.ts product-list.component.ts order.model.ts // Bad examples service-user.ts userService.ts
-
-
Comments: Use JSDoc for documenting functions, classes, and modules.
Formatting
- Indentation: Use 2 spaces for indentation.
- Line Length: Limit lines to 80 characters where possible.
- Semicolons: Always use semicolons.
- Quotes: Use single quotes for strings, except when using template literals.
Linting and Tools
- Use ESLint with the recommended TypeScript and Nx configurations.
- Prettier should be used for consistent formatting.
References
- Angular Style Guide - Official Angular style guide with best practices for Angular development
- Angular Control Flow - Official Angular documentation on the new control flow syntax (@if, @for, @switch)
- TypeScript Style Guide - TypeScript community style guide with patterns and practices
- SOLID Design Principles - Wikipedia article explaining the SOLID principles in object-oriented design
- Clean Code - Robert C. Martin's seminal book on writing clean, maintainable code