Typed Checking for Inline NgTemplateOutlet
In the previous post, we explored how to enforce strict type-checking on NgTemplateOutlet when the outlet lives on a child component. Now we turn to the scenario where the TemplateRef and the NgTemplateOutletDirective coexist in the same template.
Consider the template below, where our TemplateOutlet directive sits inside the #personRef template.
<ng-container
*ngTemplateOutlet="
personRef;
context: { $implicit: person.name, age: person.age }
">
</ng-container>
<ng-template #personRef let-name let-age="age">
{{ name }}: {{ age }}
</ng-template>
While this template functions correctly, it lacks autocompletion and type inference.
For a deeper dive into the underlying mechanics, I recommend reviewing my earlier articles on NgTemplateOutlet Type Checking and Directive Type Checking.
As demonstrated previously, a directive with a ngTemplateContextGuard can define the PersonContext for our template.
interface PersonContext {
$implicit: string;
age: number;
}
@Directive({
selector: 'ng-template[person]',
standalone: true,
})
export class PersonDirective {
static ngTemplateContextGuard(
dir: PersonDirective,
ctx: unknown
): ctx is PersonContext {
return true;
}
}
By applying this directive to our template, type inference immediately kicks in.
But what about the outlet directive itself? How do we enforce strict typing on the context it passes? The compiler currently stays silent when we supply an incorrect property to the context.
Note: At the time of writing, the built-in
ngTemplateOutletis not strictly typed, so I am using a custom directive for the remainder of this post (the implementation is available here).
This step is more involved because we must first obtain a reference to the template to access its context.
In the previous article, the read metadata property of @ContentChild handled that lookup. Here, we need to do it manually.
The strategy is to inject the template reference into our outlet directive with the correct context type. Angular's exportAs option on the directive decorator lets us expose that instance to the template.
exportAsassigns a name that templates can use to reference the directive instance.
@Directive({
selector: 'ng-template[person]',
exportAs: 'personExportAsRef', // named this way for better clarity
standalone: true,
})
export class PersonDirective {
constructor(public template: TemplateRef<PersonContext>){}
static ngTemplateContextGuard(
dir: PersonDirective,
ctx: unknown
): ctx is PersonContext {
return true;
}
}
Now we can export the directive in our template and reach its public properties.
<ng-container
*ngTemplateOutlet="
personRef.template; <!-- ref to directive TemplateRef -->
context: { $implicit: person.name, age: person.age }
">
</ng-container>
<!-- assign the directive to personRef to access internal properties -->
<ng-template #personRef="personExportAsRef" person let-name let-age="age">
{{ name }}: {{ age }}
</ng-template>
With this in place, the compiler flags both type mismatches and missing context properties.
NgTemplateOutlet and directives should now hold no secrets for you. You are equipped to build secure, typed applications for your team.
If you missed any of the preceding posts, here are the links:
- Typescript Type Predicate
- Directive Type Checking
- NgTemplateOutlet Type Checking — with @ContentChild
I hope you found a new Angular concept to add to your toolkit. If you enjoyed this, you can reach me on Medium, Twitter, or GitHub.
👉 And to accelerate your Angular learning journey, check out Angular challenges.
