AcademyAngular
Derived State and Strongly-Typed Inputs
computed() replaces a hand-managed boolean that tracks the same thing state already implies; a discriminated-union @Input() lets the compiler enforce every branch.
2 min readUpdated 3 exercises
A hand-managed boolean and computed() can disagree
The moment a component tracks isLoading as its own signal, separately from the requests it is supposedly describing, there are now two sources of truth that have to be kept in sync by hand — and overlapping async calls are exactly the case that breaks that sync:
// fragile: two overlapping searches both toggle the same flag
isLoading = signal(false);
async search(query: string): Promise<void> {
this.isLoading.set(true);
const results = await this.api.search(query);
this.isLoading.set(false); // the FIRST search's callback can fire after the second's
}If a user types quickly enough to fire two searches, the first request's .set(false) can resolve after the second request's .set(true), turning the loading flag off while a search is still genuinely in flight. The fix deletes the flag entirely and derives it instead:
activeRequests = signal(0);
isLoading = computed(() => this.activeRequests() > 0);Now there is exactly one number to get right — increment on request start, decrement on completion — and "loading" is never a separately-tracked fact that can drift from it, because computed() recalculates from the count every time, rather than being told what to be.
A discriminated union input needs the class field to actually narrow on it
Typing an @Input() as a union of tagged variants only pays off if the component's own logic branches on the tag:
type LinkVariant =
| { kind: 'internal'; route: string }
| { kind: 'external'; url: string; newTab: boolean };
@Input() link!: LinkVariant;A SmartLink that "always renders as a button, even for the external variant" typically has a template or method that checks link.route for truthiness instead of switching on link.kind — which works by accident for the internal case and silently falls through to a wrong default for every other one, because nothing forced the code to actually look at the discriminant TypeScript is offering it for free.
What interviewers check
Whether you reach for computed() instead of a second hand-toggled flag the instant a value is fully determined by other signals, and whether a discriminated union in your own code is something you branch on by its tag — not by guessing which field happens to be present.
Now practice it
1 quiz · 2 drillsReading this page is the cheap half. These are the exercises that make you use it.
- computed() as a Derived Loading GuardProDelete a hand-managed loading flag that two overlapping searches fight over, and replace it with the computed() that already exists.DrillSolve · MediumDrillSolve · Medium
- Typed @Input() with a Discriminated UnionProFix a SmartLink whose input is correctly typed as a discriminated union but always renders as a button, even for the external variant.DrillSolve · HardDrillSolve · Hard
- RxJS & Observables · Higher-Order ObservablesHigher-order observable questions at the level where derived, combined async state gets genuinely tricky.QuizTake · HardQuizTake · Hard
Read next
Services and Async State with Signals
A service is Angular's unit of shared, reusable logic — its composable — and signal() is how it holds async state a component can read reactively.
2 min
Generic Pipes and Type Guards
A pipe that stays generic over its input works for any array shape; a runtime type guard is the only thing standing between an untyped API response and a crash.
2 min