When writing a custom Angular structural directive, what do the static ngTemplateGuard_ and ngTemplateContextGuard members tell the template type checker?
answer
- static members never run
- narrowing inside the template
- the 'binding' literal
- types for let- variables
basics
~20 sThey are compile-time hints for Angular's template type checker. ngTemplateGuard_<input> narrows the bound expression inside the directive's template, via a type predicate or the literal 'binding' for truthiness; ngTemplateContextGuard declares the template context's type so let- variables get real types.
solid answer
~40 sA structural directive renders an `<ng-template>` whose contents the type checker cannot understand on its own. Two static members fill the gap. `static ngTemplateGuard_<inputName>` narrows the expression bound to that input inside the template: either a type-predicate function, such as `(dir, expr: User | Robot): expr is User`, or the literal type `'binding'`, which means "treat the bound expression as truthy" - exactly how `NgIf` declares `ngTemplateGuard_ngIf: 'binding'`. `static ngTemplateContextGuard(dir, ctx): ctx is MyContext<T>` tells the checker the context's shape, often using the directive's inferred generic, so `let-item` gets type `T` instead of `any`; `NgForOf` uses this. Neither body runs at runtime. Input guards apply wherever template bodies are type-checked; the context guard additionally needs `strictInputTypes`, which follows `strictTemplates`.
code
ts · 20 linesimport { Directive, TemplateRef, ViewContainerRef, effect, inject, input } from '@angular/core';
interface User { name: string; }
@Directive({ selector: '[appIfUser]' })
export class IfUser {
appIfUser = input<User | null>(null);
private tpl = inject(TemplateRef);
private vcr = inject(ViewContainerRef);
constructor() {
effect(() => {
this.vcr.clear();
if (this.appIfUser()) this.vcr.createEmbeddedView(this.tpl);
});
}
// Inside the template, treat the bound expression as truthy (non-null).
static ngTemplateGuard_appIfUser: 'binding';
}go deeper
Know that custom structural directives can declare static members that help the compiler type-check the template they render.
Explain the two guard kinds: an input guard that narrows via a predicate or 'binding', and a context guard that types let- variables.
Write both guards for a generic directive, name them correctly, and know the context guard depends on strictInputTypes and neither guard is checked at runtime.
Treat guards as part of a shared library's public type contract and review them like API changes, since a wrong guard misleads every consumer.
## The problem guards solve A **structural directive** such as `*appIfUser` or `*appSelect` takes an `<ng-template>` and decides when and with what context to render it. The template type checker sees the template's contents but not the directive's runtime logic, so by default: - it does not know that the directive only renders when an input is truthy or of a particular type, so expressions inside stay un-narrowed; - it does not know the shape of the **context** object the directive passes, so `let-` variables are typed `any`. **Template guards** are static, type-level declarations on the directive class that fill both gaps. Their bodies never run; only their types matter. ## `ngTemplateGuard_<input>`: narrowing an input expression The member name is `ngTemplateGuard_` followed by the input's name. It comes in two forms: 1. **Type-predicate function** - narrows the bound expression to a type: ```ts static ngTemplateGuard_actor(dir: ActorIsUser, expr: User | Robot): expr is User { return true; // never called; present to satisfy TypeScript } ``` Inside `<ng-template [appActorIsUser]="actor">`, the checker treats `actor` as `User`. 2. **The literal `'binding'`** - narrows by truthiness, which a predicate cannot express: ```ts static ngTemplateGuard_condition: 'binding'; ``` The checker behaves as if the bound expression was asserted truthy within the template. This is exactly how the built-in `NgIf` declares `static ngTemplateGuard_ngIf: 'binding'`, which is why `*ngIf="user"` removes `null` from `user` inside its template. ## `ngTemplateContextGuard`: typing the context If the directive creates the embedded view with a context object, declare its type: ```ts export interface SelectContext<T> { $implicit: T; } static ngTemplateContextGuard<T>(dir: SelectDirective<T>, ctx: unknown): ctx is SelectContext<T> { return true; } ``` Because the guard receives the directive instance's type, it can use the directive's **generic parameter**, which the checker infers from the input bindings. A `let-item` variable then has type `T`. `NgForOf` uses this pattern, and so do many component libraries. ## Preconditions and interactions | Condition | Effect on guards | | :-- | :-- | | `strictTemplates: true` (default since v22) | Input guards and the context guard both applied | | `strictInputTypes: false` set explicitly | Input guards still applied; the **context guard is not**, so `let-` variables become `any` | | `strictTemplates: false` | Embedded template bodies are not type-checked, so neither guard has any effect | ## How the checker uses them The compiler checks each template by generating a **type-check block**, a TypeScript function mirroring the template. When it meets `<ng-template [appActorIsUser]="actor">`, it emits the directive's guard as a condition around the code for the template's contents: - for a predicate guard, roughly `if (ActorIsUser.ngTemplateGuard_actor(dir, ctx.actor)) { ...template body... }`, so TypeScript narrows `ctx.actor` inside; - for a `'binding'` guard, roughly `if (ctx.actor) { ... }`, reusing the expression itself; - for a context guard, a similar condition narrows the context variable, so every `let-` variable read from it is typed. Because the type-check block is never emitted or executed, the guard bodies never run: `return true` exists only to satisfy TypeScript. ## When you need them in 2026 - The built-in control flow (`@if`, `@for`, `@switch`) is compiled directly by Angular and needs no guards; how it narrows is a separate topic. - Guards matter for **custom** structural directives - permission directives, typed repeaters, data-source wrappers - and for library authors whose directives consumers use with strict templates. - Declaring a guard is part of a directive's public type contract: a wrong predicate lies to every consumer's type checker, because nothing verifies it at runtime. ## Common mistakes - Expecting the guard's body to execute and filter values - it never runs. - Naming the member after the selector rather than the **input** name. - Returning `boolean` instead of a type predicate (`expr is User`), which narrows nothing.
- Why does Angular's NgIf declare ngTemplateGuard_ngIf as the literal 'binding' rather than a type predicate?NgIf renders when its input is truthy, and truthiness cannot be expressed as a single TypeScript type predicate over an arbitrary type. The `'binding'` literal tells the checker to reuse the bound expression itself as the guard, as if it were written in an `if` condition, so `null`, `undefined` and other falsy variants are narrowed away.
- What happens to a custom Angular directive's ngTemplateContextGuard if a project sets strictInputTypes to false?The compiler stops applying `ngTemplateContextGuard`, because it ties context guards to that setting, so the directive's `let-` variables fall back to `any` even though `strictTemplates` is still true. `ngTemplateGuard_` input guards are not tied to that flag and still narrow the bound expression.
saying these in an interview costs you the question
- The guard function runs at runtime and filters out invalid values.
- ngTemplateGuard_ must be suffixed with the directive's selector, not the input name.
- Built-in @if and @for need ngTemplateGuard_ members to narrow types.
- ngTemplateContextGuard can return a plain boolean and still narrow the context.
- Template guards work the same with strictTemplates turned off.