Is there a way to add a type assertion / annotation to a template input variable?

Viewed 4902

Background

I have a template that looks like this (I'm using some component that uses this as the basis for a repeated item, it's the <p-pickList>, but the question is not specific about that component, just as an example)

For background, let's say I have a type Foo and my component has a foos: Foo[], I'm feeding it to the <p-pickList> component under the [source] attribute and its doing the internal *ngFor for it, all I have to do is provide a template

 <ng-template let-foo pTemplate="item">
   ...
   {{ foo.anythingGoesHereWithNoWarningAndNoAutocomplete }}

However, the type information on foo seems to be lost.

I'm a big fan of type safety and I like that Intellij (or any other editor) can show me a warning if inside the template I do something like specifying an invalid attribute of foo

If I had a regular *ngFor, it would infer the type of foo

<div *ngFor="let foo of foos">
  {{ foo.autoCompleteWorksInMostIDEsAsWellAsWarningIfInvalidProp }}

Questions:

  1. Is there any syntax that will allow me to hint the type of let-foo? (and hopefully most IDE's will recognize).

  2. If I don't want to rely on IDE's, is there a way to have the ng compiler type check foo (declared by let-foo)?

tl;dr is there a syntax that let me type annotate the template input variable? e.g. something like this made up syntax?

let-foo="$implicit as Foo" or let-foo-type="Foo"?

Workaround

One silly idea is to have an identity function in my component, e.g.

identity(foo: Foo): Foo {
  return foo;
}

But doing

{{ identity(foo).fooProp }}

Is not a big improvement over

{{ (foo as Foo).fooProp }}`
5 Answers

Let's see what angular has similar to that and how it work!

<p *ngFor="let number of [{v: 101},{v: 102}, {v: 103}]">{{number.v}}</p>

We can rewrite it without * magic

<ng-template ngFor let-number [ngForOf]="[{v: 101},{v: 102}, {v: 103}]">
  <p>{{number.v}}</p>
</ng-template>

Without watchers (ngDoCheck) it can be the same as (but ngTemplateOutlet have no typecheck):

<ng-template let-number #templateRef>
  <p>{{number.v}}</p>
</ng-template>
<ng-container *ngTemplateOutlet="templateRef; context: {$implicit: {v: 101}}"></ng-container>
<ng-container *ngTemplateOutlet="templateRef; context: {$implicit: {v: 102}}"></ng-container>
<ng-container *ngTemplateOutlet="templateRef; context: {$implicit: {v: 103}}"></ng-container>

Or we can create it by ourselves

// template
<button (click)=create(templateRef)>create</button>
// TS
constructor(private _viewContainerRef: ViewContainerRef) { ... }

create(templateRef: TemplateRef<{$implicit: {v: number;}}>) {
    this._viewContainerRef.createEmbeddedView(templateRef, {$implicit: {v: 101}});
    this._viewContainerRef.createEmbeddedView(templateRef, {$implicit: {v: 102}});
    this._viewContainerRef.createEmbeddedView(templateRef, {$implicit: {v: 103}});
}

TL;DR

Template typecheck magic happens inside viewContainerRef.createEmbeddedView. (for example ngFor); But it assume what templateRef accepts.

Angular can compile in AOT:

<p *ngFor="let num of [{v:1}, {v:2}]">
  {{num.does.not.exist.completly}}
</p>

So as i understood we should assume what types templates have but do check when template is instantiated (by createEmbeddedView);

The problem is that there is no any type information. <ng-template let-foo pTemplate="item"> - is just template declaration, it is not yet bound to any type/context, just some dynamic type that has or may have anythingGoesHereWithNoWarningAndNoAutocomplete field. Without proper generics support it is not possible to have this kind of typesafety.

Found this issue and proposed solution to allow template input variables to publish type information so that IDEs can provide better completion on github here. This feature is currently under development and hopefully it'll be out in the future release of Angular. You should use Angular Language Service extension in VS Code or Sublime Text for now. You can rest assured about the extension's support because it's being worked on by the Angular team. For more info, checkout this readme and docs.

As you mentioned it can be achieved by using identity function but it will cause heavy performance issue and it'll be a mess maintaining this functionality for different types.

This can be solved by wrapping your variable inside another ng-template

This is somehow another workaround, but I liked a lot more than the other solutions here because it just adds 2 more lines of code in the HTML, of course if you're using your variable only 1 or 2 times this other solution is better. My answer:

Instead of this:

<p *ngTemplateOutlet="foo; context: {$implicit: {fooProp: 'Hello!'}}"></p>
<ng-template #foo let-args>
    This is untyped: {{ args.fooProp }}<br>
</ng-template>

Do this:

<p *ngTemplateOutlet="foo; context: {$implicit: {fooProp: 'Hello!'}}"></p>
<ng-template #foo let-untypedArgs>
    <ng-template [ngIf]="identity(untypedArgs)" let-args="ngIf">
        This is typed: {{ args.fooProp }}<br>
    </ng-template>
</ng-template>
identity(foo: Foo): Foo {
    return foo;
}

As stated already by everyone, the type assertion is noticed by the IDE when *ngFor is in use. It also works when using *ngIf. Of course there's a downside with this solution as the inner <ng-template> is rendered later because of the [ngIf].

With this, now if you add an invalid property to your context, you'll get the following compilation error which is great, here's a stackblitz demo:

Property 'newFooProp' does not exist on type 'Foo'.

I came up with this solution by just reading the documentation here:

enter image description here

I tested this solution and works on Angular 11 using vscode with angularCompilerOptions properties enableIvy and fullTemplateTypeCheck setted on true.

Also, Angular Language Service should be installed, this solution is needed even with ALS installed because the template can be called from anywhere, and so, ALS doesn't have a way to know the context params being passed. This can be easily tested in the stackblitz demo I've added, just rename fooProp to fooProp1, you'll see that the untyped example shows an empty value, and the typed one throws an error.

Related