How do `ngTemplateContextGuard` and `ngTemplateGuard_` let Angular's template type checker type a custom structural directive's context and narrow its input?
answer
- static members, never called at runtime
- context type starts as any
- a type predicate on ctx
- 'binding' means truthiness
basics
~20 sBoth are static members read only by Angular's template type checker: ngTemplateContextGuard is a type predicate that gives the template's let variables a real context type, and ngTemplateGuard_<input> narrows the bound expression inside the template.
solid answer
~40 sA structural directive may create embedded views with any context, so Angular's type checker starts the context type as `any`, and `let` variables are untyped. Declaring `static ngTemplateContextGuard<T>(dir: MyDir<T>, ctx: unknown): ctx is MyCtx<T>` tells the checker the real shape; it uses the directive's inferred generic, so `let row` gets the element type of the input. That guard is only applied when `strictTemplates` is on. Separately, `static ngTemplateGuard_<inputName>` narrows the expression bound to that input inside the template: either a type predicate function, for example narrowing `User | Robot` to `User`, or the literal type `'binding'`, which treats the expression as asserted truthy, which is how `NgIf` removes `null` from `user` inside its block. Neither body runs at runtime; they exist only in the generated type-check code.
code
ts · 30 linesimport { Directive, TemplateRef, ViewContainerRef, effect, inject, input } from '@angular/core';
export interface RepeatContext<T> {
$implicit: T;
index: number;
}
@Directive({ selector: '[appRepeat]' })
export class RepeatDirective<T> {
private readonly tpl = inject<TemplateRef<RepeatContext<T>>>(TemplateRef);
private readonly vcr = inject(ViewContainerRef);
readonly appRepeatOf = input.required<readonly T[]>();
constructor() {
effect(() => {
this.vcr.clear();
this.appRepeatOf().forEach((item, index) =>
this.vcr.createEmbeddedView(this.tpl, { $implicit: item, index }),
);
});
}
static ngTemplateContextGuard<T>(
dir: RepeatDirective<T>,
ctx: unknown,
): ctx is RepeatContext<T> {
return true; // never runs; read only by the template type checker
}
}go deeper
Recall that without a guard a custom structural directive's let variables are untyped, and that NgIf's null narrowing comes from a guard.
Explain the difference: the context guard types let variables, while an input guard narrows the expression bound to one input.
Write a generic directive whose context guard follows its input's type, and keep the runtime context and the declared interface in sync.
Treat guards as part of a shared directive's public contract, and make sure the projects consuming it run with strictTemplates so the contract is checked.
## The problem the guards solve Angular's compiler **type-checks templates** by generating TypeScript code, called a type-check block, that mirrors each template's bindings. For a structural directive this runs into a gap: the directive creates embedded views at runtime with whatever context object it likes, so the compiler cannot know the context's type. The type-check engine therefore starts every structural template's context as **`any`**, which means `let` variables are `any` and mistakes like `row.nmae` go unreported. A second gap concerns narrowing. If a directive only renders when an input is truthy, or only for a subtype, the template inside should see the narrowed type, just as TypeScript narrows inside an `if`. The checker cannot infer that from the directive's runtime code. Angular closes both gaps with two **static, type-level declarations** on the directive class. Their bodies are never executed; they exist so the generated type-check code can call them. ## Typing the context: `ngTemplateContextGuard` Declare a static type predicate that takes the directive instance and the context and asserts the context's type: - The signature is `static ngTemplateContextGuard<T>(dir: MyDir<T>, ctx: unknown): ctx is MyContext<T>`. - Because it receives the directive typed with its generic, the context can depend on what was bound to an input. A directive with `appRepeatOf = input.required<readonly T[]>()` can declare `$implicit: T`, so `let row of orders` types `row` as the order type. - The generated code wraps the template body in `if (MyDir.ngTemplateContextGuard(dirInstance, ctx)) { … }`, so everything inside sees the narrowed context. - It is applied only when **`strictTemplates`** is enabled; in the older full mode the context stays `any`. `NgFor` and `NgIf` both declare one: `NgForOf.ngTemplateContextGuard` narrows to `NgForOfContext<T, U>`, which is why `let i = index` is a `number` in strict mode. ## Narrowing an input: `ngTemplateGuard_<input>` The second guard is named after an input and comes in two forms: 1. **A type-assertion function.** `static ngTemplateGuard_actor(dir: ActorIsUser, expr: User | Robot): expr is User` tells the checker that inside the template the expression bound to `actor` is a `User`. 2. **The literal `'binding'`.** `static ngTemplateGuard_condition: 'binding'` tells the checker to use the bound expression itself as the guard, as if it had been asserted truthy. This captures truthiness, which a type predicate cannot fully express. `NgIf` declares `static ngTemplateGuard_ngIf: 'binding'`. That is why, with `strictNullChecks`, `user.name` inside `*ngIf="user"` compiles even though `user` is `User | null` outside. ## Putting it together | Guard | Declared as | Affects | Applied when | | --- | --- | --- | --- | | `ngTemplateContextGuard` | Static type-predicate method | Type of the context, and so every `let` variable | `strictTemplates` is on | | `ngTemplateGuard_x` (function) | Static type-predicate method | The expression bound to input `x` | Template type checking | | `ngTemplateGuard_x: 'binding'` | Static property typed `'binding'` | The expression bound to `x`, narrowed by truthiness | Template type checking | A typical sequence when writing a reusable directive: 1. Make the directive generic over what its main input holds. 2. Define a context interface with `$implicit` and any named properties the `let` syntax should reach. 3. Add `ngTemplateContextGuard` returning that interface for the directive's generic. 4. Add `ngTemplateGuard_` for an input only if the template renders just for a narrowed or truthy value. ## How it looks at a call site For a generic directive whose main input is `appRepeatOf`, declared with `input.required<readonly T[]>()`, and whose context guard returns `RepeatContext<T>`: - `*appRepeat="let o of orders; let i = index"` infers `T` from `orders`, so `o` is the order type and `i` is a `number`. - `{{ o.totl }}` is reported as an error on the misspelled property, instead of silently rendering nothing. - Without the guard, or with `strictTemplates` off, both `o` and `i` are `any`, and the typo passes compilation. The same mechanism is why the built-in directives feel typed in strict projects: `NgFor` and `NgIf` get their context typing and narrowing from these same public guards, so a custom directive that declares them gets the same checking. ## Pitfalls - **The guard can lie.** The checker trusts the predicate; if the directive's runtime context differs from what the guard declares, templates type-check and then fail at runtime. Keep the context object and the interface in one place. - **Wrong input name.** The suffix after `ngTemplateGuard_` must match the input's name exactly, including the selector prefix, such as `ngTemplateGuard_appHasRole`; a mismatched name is simply never used. - **Non-strict projects see nothing.** Since v22 an unset `strictTemplates` counts as enabled, but a project that sets it to `false` skips context guards, so a team may believe templates are typed when they are not.
- Why does `user.name` inside `*ngIf="user"` compile with `strictNullChecks` when `user` is `User | null`?`NgIf` declares `static ngTemplateGuard_ngIf: 'binding'`, so the type checker wraps the template body in a check on the bound expression itself. Inside, `user` is narrowed exactly as TypeScript narrows inside `if (user)`, removing `null`.
- What happens to a custom directive's `let` variables if the project turns `strictTemplates` off?The context guard is not applied, so the context stays `any` and every `let` variable is `any`. Templates still compile, but typos and wrong property accesses on those variables are no longer caught.
saying these in an interview costs you the question
- The guard methods run at runtime to validate the context object.
- Angular infers the context type from the createEmbeddedView call without any guard.
- ngTemplateGuard_ types the let variables of the template.
- The 'binding' literal means the input is bound only once.