52

How can parent component recognise type of let-content which comes from ngTemplateOutletContext? Now {{content.type}} works correctly, but IDE says:

unresolved variable type

How can I type it as Video?

parent.component.ts:

export interface Video {
  id: number;
  duration: number;
  type: string;
}

public videos: Video = [{id: 1, duration: 30, type: 'documentary'}];

parent.component.html:

<ul>
  <li *ngFor="let video of videos">
    <tile [bodyTemplate]="tileTemplate" [content]="video"></app-card>
  </li>
</ul>

<ng-template #tileTemplate let-content>
  <h5 class="tile__type">{{content.type}}</h5>
</ng-template>

tile.component.ts:

@Component({
  selector: 'tile',
  templateUrl: './tile.component.html',
  styleUrls: ['./tile.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CardComponent {
  @Input() tileTemplate: TemplateRef<any>;
  @Input() content: Video;
}

tile.component.html:

<div
...
  <ng-container
    [ngTemplateOutlet]="tileTemplate"
    [ngTemplateOutletContext]="{ $implicit: content }">
  </ng-container>
...
</div>
ssuperczynski
  • 3,190
  • 3
  • 44
  • 61
  • Does this answer your question? [Is there a way to add a type assertion / annotation to a template input variable?](https://stackoverflow.com/questions/52087168/is-there-a-way-to-add-a-type-assertion-annotation-to-a-template-input-variable) – luiscla27 Apr 12 '21 at 22:59

5 Answers5

33

There is no type inference for let-* variables. The let- context is part of the micro syntax parser for Angular, and an IDE can not infer the type as there is no clear origin.

https://gist.github.com/mhevery/d3530294cff2e4a1b3fe15ff75d08855

You can try to silence the IDE warning using $any()

https://angular.io/guide/template-syntax#the-any-type-cast-function

<ng-template #tileTemplate let-content>
  <h5 class="tile__type">{{$any(content).type}}</h5>
</ng-template>

You can force type inference by using a function

<ng-template #tileTemplate let-content>
  <h5 class="tile__type">{{toVideo(content).type}}</h5>
</ng-template>

public toVideo(value: any): Video { return value as Video; }
Reactgular
  • 52,335
  • 19
  • 158
  • 208
  • 4
    But that will cause the ngZone to call the function in every run. I don't think that it's worth the performance drawback if you have this in every template – Musab Kurt Feb 23 '21 at 12:00
  • 6
    @MusabKurt yes it will. An alternative is to use a pipe, which is more handy because you can use the pipe in different component templates. I wish the Angular team would introduce a way for us to define custom $type() methods for templates. – Reactgular Feb 23 '21 at 14:20
  • 4
    You can probably get away with this method if you're using `changeDetection: ChangeDetectionStrategy.OnPush` – Simon_Weaver Feb 26 '21 at 19:04
  • No much difference to use a function as simple value getter or a property in terms of performance. See this discussion https://github.com/indepth-dev/community/discussions/133 – rossoneri Nov 25 '21 at 14:44
  • That is not true (at least nowadays), angular is able to properly define the type for template context using ngTemplateContextGuard. Please see the answer below with the ultimate solution for this by @lordblackhole – carecki Feb 23 '22 at 22:14
29

I created a helper directive to solve this.

import { Directive, Input, TemplateRef } from '@angular/core';

@Directive({selector: 'ng-template[typedTemplate]'})
export class TypedTemplateDirective<TypeToken> {

  // how you tell the directive what the type should be
  @Input('typedTemplate')
  typeToken: TypeToken;

  // the directive gets the template from Angular
  constructor(private contentTemplate: TemplateRef<TypeToken>) {
  }

  // this magic is how we tell Angular the context type for this directive, which then propagates down to the type of the template
  static ngTemplateContextGuard<TypeToken>(dir: TypedTemplateDirective<TypeToken>, ctx: unknown): ctx is TypeToken{ return true; }
}

Use it like this

<!-- typedTemplate is the directive, typeToken is an object on our component -->
<ng-template #someTemplate [typedTemplate]="typeToken" let-param="param">
  {{param}}
</ng-template>

And in the component

// here we create typeToken. the value doesn't matter as it's never used, but the type of this variable defines the types of all template parameters. 
typeToken: { param: string };
LordBlackhole
  • 401
  • 4
  • 5
  • 1
    This is a nice workaround, using Angular/TS thingy. Though you should change your static methods "TypeToken" to another token. Yours is shadowing the token from the class, and can have a different name. Input can also directly be named from the directive name "typedTemplate", and the unused property "private contentTemplate" should be prefixed by "_" for TS to not rise an error :) – Jean-Rémi Aug 04 '21 at 15:23
  • 2
    Not working, the object param is empty – Ania Oct 15 '21 at 07:07
  • 1
    What's the point of writing a long directive if we still have to declare a token – user1034912 Mar 22 '22 at 02:57
  • 2
    How this works with multiple params – Leonardo Rick Jul 28 '22 at 16:07
  • They are just fields on the typeToken. `typeToken: { something: string, otherThing: number }` `` – LordBlackhole Aug 01 '22 at 18:24
  • Nice workaround, but a bit problematic. I usually want to use this with an existing type. The problem is the template does not see types. A big probelm. With this approach we still have to somehow duplicate the type definition inside the ts to make it accessible. I wish the templates would inherit the ts just like in .Net. This will also allow to encapsulate instead of defining everyting as public instead of protected (which is another big problem). I also wish angular would give a builtin solution for accessing types from the template (similar to usings in .Net templates) – Cesar Jan 15 '23 at 14:46
  • @Cesar just keep the "typeToken" as a public variable in the component using the template then refer it: `` – Flavien Volken Jan 23 '23 at 15:09
  • This is the best answer. The only other way to define a typed variable in the template is using a combination of a function to cast your variable then using ngIf. The problem with that is if your variable's value happens to be `false`, `undefined`, `null`, `0`, or an empty string, then your ngIf is going to be false and not render the element. – Jordan9232 Feb 10 '23 at 03:11
7

There is an easy workaround to get the IDE to play along and also have code completion using a user-defined type guard:

Create a function in your class which takes the variable as an argument and returns the same variable:

export class CardComponent {
  ...
  public video = (item: Video) => item;
}

Now simply wrap the variable in your template with the function:

<h5 class="tile__type">{{video(content).type}}</h5>
Rei Mavronicolas
  • 1,387
  • 9
  • 12
  • 3
    best workaround I've seen - with a 1 char variable name it barely clutters the template. But it's hard to believe it's necessary... – Joakim Aug 10 '20 at 13:10
  • 8
    This is a repost of the solution @Reactangular gave at the accepted answer: https://stackoverflow.com/a/55458722/1657465 – luiscla27 Apr 12 '21 at 23:46
  • 3
    But this introduces a performance issue, doesn't it? You changed an object reference into a method that must be run on every cycle. In Angular this should be done with a pure pipe to explicilty instruct the framework to calculate it once. – Cesar Aug 22 '22 at 08:59
3

A solutión without the creation of a new directive, this is somehow another workaround very similar to @Reacangular's.


This can be solved by wrapping your variable inside another ng-template, but I liked a lot more than the other solutions 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 @Reactangular answer is better. My answer:

Instead of this:

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

Do this:

<ng-template *ngTemplateOutlet="foo; context: {$implicit: {fooProp: 'Hello!'}}"></ng-template>
<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;
}

The type assertion is noticed by the IDE when *ngFor or *ngIf is in use. The downside with this solution is that 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'.


As stated in comments, just like the accepted answer; this solution has the downward of calling ngZone every lifecycle, It might be recommended only when using together with ChangeDetectionStrategy.OnPush.

luiscla27
  • 4,956
  • 37
  • 49
1

You type any template variable using an *ngIf and a function to type it

<ng-container *ngIf="asMyType(anyType) as myType">
  <!-- myType is typed here -->
</ng-container>
const asMyType = (something: unknown) => something as myType;

So you can apply this same method inside a ng-template to type a variable

<ng-template let-my-type="my-type">
  <ng-container *ngIf="asMyType(my-type) as myType">
    <!-- myType is typed here -->
  </ng-container>
</ng-template>
Joel Duckworth
  • 5,455
  • 3
  • 20
  • 21