ssuperczynski
ssuperczynski

Reputation: 3416

ng-template - typed variable

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>

Upvotes: 63

Views: 41663

Answers (6)

luiscla27
luiscla27

Reputation: 6409

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-container *ngIf="identity(untypedArgs) as args">
        This is typed: {{ args.fooProp }}<br>
    </ng-container>
</ng-template>
identity(foo: Foo): Foo {
    return foo;
}

NEWER SYNTAX:
<ng-template #foo let-untypedArgs>
    @if (identity(untypedArgs); as args) {
        This is typed: {{ args.fooProp }}<br>
    }
</ng-template>
EVEN NEWER SYNTAX:
<ng-template #foo let-untypedArgs>
    @let args = identity(untypedArgs)
    This is typed: {{ args.fooProp }}<br>        
</ng-template>

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

Upvotes: 4

El Mac
El Mac

Reputation: 3419

I have another workaround that works since Angular 18 using the @let statement.

In Typescript:

asPerson(person: Person) : Person {
   return person;
}

In your Angular Template

<ng-template let-param="untypedPerson">
  @let person = asPerson(untypedPerson);
  {{person.name}}
</ng-template>

Upvotes: 3

Joel Duckworth
Joel Duckworth

Reputation: 6401

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>

Upvotes: 3

LordBlackhole
LordBlackhole

Reputation: 541

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 };

Upvotes: 43

Rei Mavronicolas
Rei Mavronicolas

Reputation: 1435

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>

Upvotes: 8

Reactgular
Reactgular

Reputation: 54741

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; }

Upvotes: 37

Related Questions