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.

No 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.

Correct IDE type inference

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 ngTemplateOutlet is not strictly typed, so I am using a custom directive for the remainder of this post (the implementation is available here).

templateOutlet definition with wrong context and no compiler error

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.

exportAs assigns 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.

compiler error on context wrong type compiler error on context missing params

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:


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.