# 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. ```typescript // 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. ```typescript // 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. ```typescript // 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. ```typescript // 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. ```typescript // 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. - **Standalone by Default**: Components and directives are standalone by default. The `standalone: true` flag is unnecessary. Only specify `standalone: false` when a component or directive explicitly needs to be part of an NgModule. ### TypeScript Enhancements - **Strict Type Checking**: Enable strict mode (`strict: true`) and avoid excessive use of `any`. - **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: true` in tsconfig.json - Avoid `any` unless absolutely necessary - Use `unknown` instead of `any` when type is truly unknown - Always specify return types for functions - Use type inference for variable declarations where types are obvious ```typescript // 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 ``` - **Interfaces and Types**: - Prefer `interface` over `type` for object definitions - Use `type` for unions, intersections, and mapped types - Follow Angular's naming convention: Don't prefix interfaces with 'I' (use `ComponentProps` not `IComponentProps`) - Extend interfaces instead of repeating properties - Use readonly modifiers where appropriate ```typescript // 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; }; ``` - **Enums and Constants**: - Prefer this order of implementation (from most to least preferred): 1. `const enum` for better compile-time performance 2. Object literals with `as const` for runtime flexibility 3. Regular `enum` only when necessary for runtime access - **When to use each approach**: - Use `const enum` for internal application enumerations that don't need runtime access - Use `const object as const` when values need to be inspected at runtime or exported in an API - Use regular `enum` only when runtime enumeration object access is required ```typescript // 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 ```typescript // Alternative approach using union types export type StatusType = 'not-set' | 'success'; ``` - **Functions and Methods**: - Always export functions as arrow function expressions (const) instead of function declarations - 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 ```typescript // 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 */ export const getUser = (id: string, includeDetails = false): Promise => { // ...implementation }; // Bad export function getUser(id) { // ...implementation } ``` // Reason for using arrow function expressions: // 1. More consistent with modern JavaScript practices // 2. Easier to mock in unit tests // 3. Avoids 'this' binding issues - **Generics**: - Use meaningful type parameter names (e.g., `T` for type, `K` for key) - Constrain generic types when possible using `extends` - Document generic parameters using JSDoc Example: ```typescript // Good interface UserProps { id: string; name: string; } interface AdminProps extends UserProps { permissions: string[]; } const enum UserRole { Admin = 'ADMIN', User = 'USER', } const getUser = (id: string): Promise => { // ...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 ```typescript // 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`). ```html
@if (user) {

Welcome, {{ user.name }}!

} @else if (isLoading) {

Loading user data...

} @else {

Please log in

}
    @for (item of items; track item.id) {
  • {{ item.name }}
  • } @empty {
  • No items available
  • }
@switch (userRole) { @case ('admin') { } @case ('manager') { } @default { } }

Welcome, {{ user.name }}!

Loading user data...

Please log in

  • {{ item.name }}
  • No items available
``` - When using `@for`, always specify the `track` expression to optimize rendering performance: - Use a unique identifier property (like `id` or `uuid`) when available - Only use `$index` for static collections that never change - Avoid using non-unique properties that could result in DOM mismatches - Leverage contextual variables in `@for` blocks: - `$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 ```html @for (item of items; track item.id; let i = $index, isLast = $last) {
  • {{ i + 1 }}. {{ item.name }}
  • } ``` - Use the `@empty` block with `@for` to handle empty collections gracefully - Store conditional expression results in variables for clearer templates: ```html @if (user.permissions.canEditSettings; as canEdit) { } ``` ## Error Logging Proper error logging is critical for diagnosing issues in production applications. Always use the `@isa/core/logging` module for logging throughout the application. ### Basic Logging Guidelines - Always use the appropriate log level for the situation: - `trace` - For fine-grained debugging information - `debug` - For development-time debugging information - `info` - For general runtime information - `warn` - For potential issues that don't interrupt operation - `error` - For errors that affect functionality ### Setting Up Logging Use the logger factory function to create loggers in your components and services: ```typescript import { logger } from '@isa/core/logging'; @Component({ selector: 'app-example', templateUrl: './example.component.html', }) export class ExampleComponent implements OnInit { // Create a private logger instance #logger = logger(); constructor(private userService: UserService) {} ngOnInit(): void { this.#logger.info('Component initialized'); } } ``` ### Context-Aware Logging Always provide relevant context when logging: ```typescript // Good - With detailed context this.#logger.error( 'Failed to load user data', error, { userId: '123', attempt: 2, source: 'UserProfileComponent' } ); // Bad - No context for troubleshooting this.#logger.error('Failed to load user data'); ``` ### Component and Service Level Context Use the `provideLoggerContext` to set component-level context: ```typescript @Component({ selector: 'app-user-profile', templateUrl: './user-profile.component.html', providers: [ provideLoggerContext({ component: 'UserProfile', section: 'Account' }) ] }) export class UserProfileComponent { #logger = logger(); // All logs from this component will include the context updateProfile(): void { this.#logger.info('Updating user profile'); // Log output will include { component: 'UserProfile', section: 'Account' } } } ``` ### Error Handling Best Practices 1. **Always log the original Error object**: ```typescript // Good - Includes the original error with stack trace try { // risky operation } catch (error) { this.#logger.error('Operation failed', error, { context: 'additional data' }); } // Bad - Loses the stack trace and error details try { // risky operation } catch (error) { this.#logger.error(`Operation failed: ${error}`); } ``` 2. **Structure error handling with contextual information**: ```typescript saveData(data: UserData): void { this.userService.save(data).pipe( catchError((error) => { this.#logger.error( 'Failed to save user data', error, { userId: data.id, dataSize: JSON.stringify(data).length, operation: 'saveData' } ); // Handle the error appropriately return throwError(() => new Error('Failed to save data')); }) ).subscribe(); } ``` 3. **Use log levels appropriately**: ```typescript // Debug information during development this.#logger.debug('Processing data batch', { batchSize: items.length }); // General information during runtime this.#logger.info('User logged in', { userId: user.id }); // Potential issues that don't break functionality this.#logger.warn('API response slow', { responseTime: '2500ms', endpoint: '/users' }); // Errors that affect functionality this.#logger.error('Payment processing failed', error, { orderId: order.id }); ``` 4. **In RxJS operators, use tap for logging**: ```typescript return this.http.get('/api/users').pipe( tap({ next: (users) => this.#logger.info('Users loaded', { count: users.length }), error: (error) => this.#logger.error('Failed to load users', error) }), catchError((error) => { this.#logger.error('Error in users API call', error, { retry: true }); return this.fallbackUserService.getUsers(); }) ); ``` 5. **Log lifecycle events when relevant**: ```typescript export class ImportantComponent implements OnInit, OnDestroy { #logger = logger(); ngOnInit(): void { this.#logger.debug('Component initialized'); } ngOnDestroy(): void { this.#logger.debug('Component destroyed'); } } ``` ### Performance Considerations - Avoid expensive operations in log messages in production: ```typescript // Bad - Performs expensive operation even if debug level is disabled this.#logger.debug('Data state:', JSON.stringify(this.largeDataObject)); // Good - Only performs expensive operation if needed if (isDevMode()) { this.#logger.debug('Data state:', { data: this.largeDataObject }); } ``` - Consider log level in production environments: ```typescript // In app.config.ts export const appConfig: ApplicationConfig = { providers: [ // Other providers... provideLogging( withLogLevel(isDevMode() ? LogLevel.Debug : LogLevel.Warn), withSink(ConsoleLogSink) ) ] }; ``` ## 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](https://angular.dev/style-guide) - Official Angular style guide with best practices for Angular development - [Angular Control Flow](https://angular.dev/guide/templates/control-flow) - Official Angular documentation on the new control flow syntax (@if, @for, @switch) - [TypeScript Style Guide](https://ts.dev/style/) - TypeScript community style guide with patterns and practices - [SOLID Design Principles](https://en.wikipedia.org/wiki/SOLID) - Wikipedia article explaining the SOLID principles in object-oriented design - [Clean Code](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) - Robert C. Martin's seminal book on writing clean, maintainable code