AcademyAngular
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 readUpdated 3 exercises
A pipe typed <T> works for whatever array is handed to it
A pipe pinned to a concrete type only ever transforms that one shape; a generic pipe stays useful across every model the app has:
@Pipe({ name: 'unique' })
export class UniquePipe implements PipeTransform {
transform<T>(items: T[], keyFn: (item: T) => unknown): T[] {
const seen = new Set<unknown>();
return items.filter((item) => {
const key = keyFn(item);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
}A "unique" pipe that never removes a single duplicate is, very often, deduplicating with a plain Set<T> over the objects themselves instead of over a derived key — a Set compares objects by reference, so two separately-created objects with identical fields are never equal to it, no matter how many fields they share. Deduplicating by keyFn(item) — an id, a slug, whatever field actually identifies "the same thing" — is what makes the comparison mean anything.
Pure pipes recompute only when their input reference changes
An Angular pipe is pure by default, meaning it re-runs only when the reference passed to it changes, not when something inside that reference mutates — mutating an array in place and expecting a pure pipe to notice is the same category of bug as OnPush change detection missing an in-place mutation, because both rely on identity, not deep equality.
A type guard has to check every field it claims to guarantee
function isUser(value: unknown): value is User is only trustworthy if its body actually inspects the fields the User type promises — a guard that checks typeof value === 'object' && value !== null and stops there has verified almost nothing, yet TypeScript will happily narrow to User on the strength of that return type alone:
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as User).id === 'string' &&
typeof (value as User).email === 'string'
);
}Malformed API responses that "get through" a guard are exactly this gap: the guard's declared return type asserts a shape the guard's own body never actually confirmed, so a response missing email still passes, and every caller downstream trusts a type that was never really checked.
What interviewers check
Whether you dedupe or compare by key instead of by reference the moment objects are involved, and whether a type guard you write actually checks every field its return type claims to guarantee — the gap between the two is invisible to the compiler and visible only in production.
Now practice it
1 quiz · 2 drillsReading this page is the cheap half. These are the exercises that make you use it.
- A Generic PipeProFix a "unique" pipe that never removes a single duplicate, because a plain Set compares the objects by reference.DrillSolve · HardDrillSolve · Hard
- A Runtime Type GuardProFix a type guard that lets malformed API responses through because it only ever checks that a value "is an object."DrillSolve · HardDrillSolve · Hard
- Router · Route Config & LinksRoute configuration questions — a different surface, but the same discipline of trusting only what was actually checked.QuizTake · EasyQuizTake · Easy
Read next
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
OnPush Change Detection and model()
OnPush skips a subtree until an @Input() reference actually changes; model() gives a component a two-way custom binding without a separate @Output() wiring.
2 min