|
| 1 | +# Angular + TypeScript Frontend Rules |
| 2 | + |
| 3 | +Concise, practical rules for building maintainable, performant, and accessible Angular apps with modern TypeScript and Angular features. |
| 4 | + |
| 5 | +## Context |
| 6 | + |
| 7 | +Guidance for day-to-day Angular component, template, service, and state design using the latest standalone APIs and signals. |
| 8 | + |
| 9 | +*Applies to:* Angular applications and libraries (v16+), SPA/MPA frontends, design systems |
| 10 | +*Level:* Operational/Tactical |
| 11 | +*Audience:* Frontend engineers, tech leads, reviewers |
| 12 | + |
| 13 | +## Core Principles |
| 14 | + |
| 15 | +1. **Type safety by default:** Enable strict typing, prefer inference, and avoid unsafe escape hatches. |
| 16 | +2. **Idiomatic modern Angular:** Prefer standalone APIs, signals, native control flow, and `inject()`. |
| 17 | +3. **Simplicity and SRP:** Keep components/services small, focused, and predictable. |
| 18 | +4. **Performance-first UI:** OnPush change detection, reactive patterns, and lazy loading by default. |
| 19 | + |
| 20 | +## Rules |
| 21 | + |
| 22 | +### Must Have (Critical) |
| 23 | +Non-negotiable rules that must always be followed. Violation of these rules should block progress. |
| 24 | + |
| 25 | +- **RULE-001:** Use strict TypeScript; avoid `any` and prefer `unknown` when type is uncertain; use type inference when obvious. |
| 26 | +- **RULE-002:** Use standalone components; do NOT set `standalone: true` in Angular decorators (it's the default). |
| 27 | +- **RULE-003:** Use signals for component/local state; use `set`/`update` only; do NOT use `mutate`. |
| 28 | +- **RULE-004:** Use `computed()` for derived state. |
| 29 | +- **RULE-005:** Use `input()` and `output()` functions instead of decorators. |
| 30 | +- **RULE-006:** Set `changeDetection: ChangeDetectionStrategy.OnPush` on all components. |
| 31 | +- **RULE-007:** Use native template control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`. |
| 32 | +- **RULE-008:** Do NOT use `@HostBinding` or `@HostListener`; define host bindings/listeners in the `host` property of `@Component`/`@Directive`. |
| 33 | +- **RULE-009:** Do NOT use `ngClass`/`ngStyle`; use `class` and `style` bindings instead. |
| 34 | +- **RULE-010:** Prefer Reactive Forms over Template-driven Forms for new code. |
| 35 | +- **RULE-011:** Use `inject()` for dependency injection in services and where appropriate. |
| 36 | +- **RULE-012:** Use `providedIn: 'root'` for singleton services. |
| 37 | +- **RULE-013:** Implement lazy loading for feature routes. |
| 38 | +- **RULE-014:** Use `NgOptimizedImage` for static images; do NOT use it for inline base64 images. |
| 39 | +- **RULE-015:** Keep components and services single-responsibility and small. |
| 40 | +- **RULE-016:** In template control flow, do not use `as` expressions in `@else if (...)`; refactor to compute values beforehand. |
| 41 | + |
| 42 | +### Should Have (Important) |
| 43 | +Strong recommendations that should be followed unless there's a compelling reason not to. |
| 44 | + |
| 45 | +- **RULE-101:** Prefer inline templates for small components; use separate files for complex markup. |
| 46 | +- **RULE-102:** Keep templates simple; move complex logic to TypeScript. |
| 47 | +- **RULE-103:** Keep state transformations pure and predictable. |
| 48 | +- **RULE-104:** Use the `async` pipe to handle Observables in templates; avoid manual `subscribe()` for rendering. |
| 49 | +- **RULE-105:** Design services around a single responsibility; avoid god-services. |
| 50 | + |
| 51 | +### Could Have (Preferred) |
| 52 | +Best practices and preferences that improve quality but are not blocking. |
| 53 | + |
| 54 | +- **RULE-201:** Favor explicit `readonly` and immutable patterns where it clarifies intent. |
| 55 | +- **RULE-202:** Co-locate small, leaf components with their feature; promote only when reused. |
| 56 | +- **RULE-203:** Document public component inputs/outputs with concise JSDoc for design system surfaces. |
| 57 | + |
| 58 | +## Patterns & Anti-Patterns |
| 59 | + |
| 60 | +### ✅ Do This |
| 61 | +Concrete examples of what good implementation looks like |
| 62 | + |
| 63 | +```typescript |
| 64 | +// Component with signals, computed state, OnPush, host bindings, native control flow, and class/style bindings |
| 65 | +import { Component, ChangeDetectionStrategy, input, output, signal, computed } from '@angular/core'; |
| 66 | + |
| 67 | +@Component({ |
| 68 | + selector: 'app-counter', |
| 69 | + // no `standalone: true` (default) |
| 70 | + changeDetection: ChangeDetectionStrategy.OnPush, |
| 71 | + host: { |
| 72 | + class: 'counter', |
| 73 | + '(click)': 'onClick()' |
| 74 | + }, |
| 75 | + template: ` |
| 76 | + @if (count() > 0) { |
| 77 | + <span class="badge" [class.is-large]="isLarge()">{{ label() }}: {{ count() }}</span> |
| 78 | + } @else { |
| 79 | + <span class="badge is-empty">Empty</span> |
| 80 | + } |
| 81 | + <button type="button" (click)="inc()">Inc</button> |
| 82 | + ` |
| 83 | +}) |
| 84 | +export class CounterComponent { |
| 85 | + readonly label = input<string>('Count'); |
| 86 | + readonly changed = output<number>(); |
| 87 | + |
| 88 | + private readonly size = input<'sm' | 'lg'>('sm'); |
| 89 | + readonly count = signal(0); |
| 90 | + readonly isLarge = computed(() => this.size() === 'lg'); |
| 91 | + |
| 92 | + inc() { |
| 93 | + this.count.update(c => c + 1); |
| 94 | + this.changed.emit(this.count()); |
| 95 | + } |
| 96 | + |
| 97 | + onClick() { |
| 98 | + // handle host click via host listener in metadata |
| 99 | + } |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +```html |
| 104 | +<!-- Template using async pipe and native control flow --> |
| 105 | +<div> |
| 106 | + @if (user$ | async; as user) { |
| 107 | + <h3>{{ user.name }}</h3> |
| 108 | + } @else { |
| 109 | + <app-skeleton></app-skeleton> |
| 110 | + } |
| 111 | +</div> |
| 112 | +``` |
| 113 | + |
| 114 | +### ❌ Don't Do This |
| 115 | +Concrete examples of what to avoid |
| 116 | + |
| 117 | +```typescript |
| 118 | +// Anti-patterns: decorators for IO, HostBinding/HostListener, mutate(), star control flow, ngClass/ngStyle |
| 119 | +import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, HostBinding, HostListener, signal } from '@angular/core'; |
| 120 | + |
| 121 | +@Component({ |
| 122 | + selector: 'app-bad', |
| 123 | + standalone: true, // ❌ don't set; it's default |
| 124 | + changeDetection: ChangeDetectionStrategy.Default, // ❌ should be OnPush |
| 125 | + template: ` |
| 126 | + <div *ngIf="count() > 0" [ngClass]="{ 'is-large': large }" [ngStyle]="{ color: color }"> |
| 127 | + {{ label }}: {{ count() }} |
| 128 | + </div> |
| 129 | + ` |
| 130 | +}) |
| 131 | +export class BadComponent { |
| 132 | + @Input() label!: string; // ❌ use input() |
| 133 | + @Output() changed = new EventEmitter<number>(); // ❌ use output() |
| 134 | + |
| 135 | + @HostBinding('class.bad') bad = true; // ❌ use host metadata |
| 136 | + @HostListener('click') onClick() {} // ❌ use host metadata |
| 137 | + |
| 138 | + color = 'red'; |
| 139 | + large = false; |
| 140 | + count = signal(0); |
| 141 | + |
| 142 | + inc() { |
| 143 | + this.count.mutate(c => { c++; }); // ❌ don't use mutate |
| 144 | + this.changed.emit(this.count()); |
| 145 | + } |
| 146 | +} |
| 147 | +``` |
| 148 | + |
| 149 | +```html |
| 150 | +<!-- Invalid control flow pitfall --> |
| 151 | +@if (getUser() as user) { |
| 152 | + <div>{{ user.name }}</div> |
| 153 | +} @else if (getAccount() as account) { <!-- ❌ invalid: as in @else if --> |
| 154 | + <div>{{ account.id }}</div> |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +```typescript |
| 159 | +// Avoid manual subscribe for template rendering |
| 160 | +userService.user$.subscribe(u => this.user = u); // ❌ prefer async pipe |
| 161 | +``` |
| 162 | + |
| 163 | +## Decision Framework |
| 164 | + |
| 165 | +*When rules conflict:* |
| 166 | +1. Favor type safety, correctness, and accessibility over convenience. |
| 167 | +2. Prefer modern Angular primitives (standalone, signals, `inject()`, native control flow) over legacy patterns. |
| 168 | +3. Choose the simplest solution that satisfies SRP and performance (OnPush, lazy loading). |
| 169 | + |
| 170 | +*When facing edge cases:* |
| 171 | +- Legacy or third-party constraints: wrap/adapter pattern; isolate exceptions locally. |
| 172 | +- Dynamic styling/events: prefer explicit `class`/`style` bindings and `host` metadata; avoid `ngClass`/`ngStyle` and decorators. |
| 173 | +- Template complexity: extract components/pipes or move logic to TS until templates are declarative. |
| 174 | + |
| 175 | +## Exceptions & Waivers |
| 176 | + |
| 177 | +*Valid reasons for exceptions:* |
| 178 | +- Interoperating with legacy modules/libraries that require deprecated patterns. |
| 179 | +- Temporary migration windows while incrementally adopting signals/standalone APIs. |
| 180 | +- Performance or security constraints that necessitate an alternative approach with measurements. |
| 181 | + |
| 182 | +*Process for exceptions:* |
| 183 | +1. Document the exception, rationale, and scope in an ADR or README. |
| 184 | +2. Obtain tech lead approval. |
| 185 | +3. Time-box the exception and add a cleanup task. |
| 186 | + |
| 187 | +## Quality Gates |
| 188 | + |
| 189 | +- **Automated checks:** TS `strict` enabled; ESLint rules for Angular best practices; template parser rules to flag `*ngIf/*ngFor`, `@HostBinding/@HostListener`, `ngClass/ngStyle` usage; CI check for OnPush and use of `input()/output()` and `inject()` patterns. |
| 190 | +- **Code review focus:** Standalone usage (no explicit `standalone: true`), signals and computed correctness, OnPush, host metadata vs decorators, native control flow, DI via `inject()`, Reactive Forms, lazy-loaded routes, NgOptimizedImage usage, async pipe usage. |
| 191 | +- **Testing requirements:** Unit tests around computed state; component change detection with OnPush; service DI via `inject()`; route lazy loading verified via router configuration tests; template tests validating async pipe rendering. |
| 192 | + |
| 193 | +## Related Rules |
| 194 | + |
| 195 | +- rules/platform/typescript.instructions.md - Complementary TypeScript guidance |
| 196 | +- rules/code-quality.mdc - Actionable review comments |
| 197 | +- rules/clean-code.mdc - General maintainability practices |
| 198 | + |
| 199 | +## References |
| 200 | + |
| 201 | +- [Angular Signals](https://angular.dev/guide/signals) - Modern reactive state management |
| 202 | +- [Standalone APIs](https://angular.dev/guide/standalone-components) - Simplified component architecture |
| 203 | +- [Control Flow](https://angular.dev/guide/template-control-flow) - Native template control structures |
| 204 | +- [Dependency Injection and inject()](https://angular.dev/guide/di) - Modern DI patterns |
| 205 | +- [Reactive Forms](https://angular.dev/guide/forms) - Type-safe form handling |
| 206 | +- [Host Bindings/Listeners](https://angular.dev/guide/directives#host-listeners-and-host-bindings) - Component host interactions |
| 207 | +- [NgOptimizedImage](https://angular.dev/guide/image-directive) - Performance-optimized images |
| 208 | +- [Async Pipe](https://angular.dev/api/common/AsyncPipe) - Observable template integration |
| 209 | + |
| 210 | +--- |
| 211 | + |
| 212 | +## TL;DR |
| 213 | + |
| 214 | +Build with strict typing, modern Angular primitives, and simple, SRP-aligned components. |
| 215 | + |
| 216 | +*Key Principles:* |
| 217 | +- Type-safe by default; prefer inference. |
| 218 | +- Use standalone, signals, `inject()`, and native control flow. |
| 219 | +- Keep things small, predictable, and OnPush. |
| 220 | + |
| 221 | +*Critical Rules:* |
| 222 | +- Don't set `standalone: true`; use signals with `set`/`update` and `computed()`; use `input()`/`output()`; OnPush; no `@HostBinding/@HostListener`, no `ngClass/ngStyle`. |
| 223 | +- Use native `@if/@for/@switch`, Reactive Forms, `inject()`, `providedIn: 'root'`, lazy routes, and the `async` pipe for Observables. |
| 224 | +- Use `NgOptimizedImage` for static images; not for inline base64; avoid `as` in `@else if`. |
| 225 | + |
| 226 | +*Quick Decision Guide:* |
| 227 | +When unsure, choose the modern Angular primitive that keeps templates declarative and components small; optimize for OnPush and type safety. |
0 commit comments