mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
736 lines
21 KiB
Markdown
736 lines
21 KiB
Markdown
# 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<User> => {
|
|
// ...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 = <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
|
|
|
|
```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
|
|
<!-- 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 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
|
|
<!-- 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 `@empty` block with `@for` to handle empty collections gracefully
|
|
- Store conditional expression results in variables for clearer templates:
|
|
|
|
```html
|
|
<!-- Good - Storing expression result in variable -->
|
|
@if (user.permissions.canEditSettings; as canEdit) {
|
|
<button [disabled]="!canEdit">Edit Settings</button>
|
|
}
|
|
```
|
|
|
|
## 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<User[]>('/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
|