Template for typed variable - `ng-template`

How can the parent component correctly identify the type of let-content that is coming from ngTemplateOutletContext? The current usage of {{content.type}} works as expected, but my IDE is showing:

unresolved variable type

Is there a way to specify the type 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>

Answer №1

I developed a custom directive to address this issue.

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

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

  // specifying the type for the directive
  @Input('typedTemplate')
  typeToken: TypeToken;

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

  // defining the context type for the directive and template
  static ngTemplateContextGuard<TypeToken>(dir: TypedTemplateDirective<TypeToken>, ctx: unknown): ctx is TypeToken{ return true; }
}

Usage example:

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

In the component:

// creating typeToken to define parameter types
typeToken: { param: string };

Answer №2

When it comes to let-* variables, type inference is not available. The Angular micro syntax parser incorporates the let- context, making it challenging for IDEs to deduce the type due to the lack of a clear origin.

To address this issue, you can attempt to suppress the IDE warning by utilizing $any()

For more information, check out 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>

If you wish to enforce type inference, you can achieve this by implementing 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; }

Answer №3

To ensure your IDE cooperates and offers code completion, utilize a custom type guard:

Include a method in your class that accepts a variable as a parameter and returns the same variable:

export class PlayerComponent {
  ...
  public player = (selected: Player) => selected;
}

Then, simply apply the function to the variable in your template:

<p class="selected__player">{{player(selection).name}}</p>

Answer №4

An alternative approach to creating a new directive, this method is similar to another workaround mentioned by @Reacangular.


To solve this issue, you can wrap your variable inside an additional ng-template. I prefer this solution over others as it only requires adding 2 more lines of code in the HTML. Of course, if you are using your variable only once or twice, the approach from @Reactangular's answer might be better. Here's my solution:

Instead of using:

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

Try this instead:

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

The type assertion is recognized by the IDE when using *ngFor or *ngIf. One drawback of this solution is that the inner <ng-container> is rendered later due to the *ngIf statement.

With this change, now if you add an invalid property to your context, you will receive a compilation error, which is beneficial. Check out the demo on StackBlitz:

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


As noted in the comments, similar to the accepted answer, this solution has the downside of calling ngZone every lifecycle. It may be advisable to use this in conjunction with ChangeDetectionStrategy.OnPush.

Answer №5

By utilizing an *ngIf directive along with a function, you can type any template variable

<ng-container *ngIf="asMyType(anyType) as myType">
  <!-- Type definition of myType goes here -->
</ng-container>
const asMyType = (something: unknown) => something as myType;

This strategy can also be implemented within an ng-template to assign a type to a particular variable

<ng-template let-my-type="my-type">
  <ng-container *ngIf="asMyType(my-type) as myType">
    <!-- Typed content for myType is defined here -->
  </ng-container>
</ng-template>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Using ngTemplateOutlet to pass ng-template to a child component in Angular 5

I am looking to develop a versatile component that can utilize custom templates for data rendering, while consolidating the business logic to prevent redundancy. Imagine this use case: a paginated list. The pagination logic should be housed within the com ...

having difficulties in separating the mat-table header

It may seem like a basic question, but I'm having trouble figuring it out. I'm not sure if this requires a CSS change or something else. I'm looking to add a divider between my table header similar to the one highlighted in the screenshot be ...

Tips for sidestepping CORS issues when using sendFile in Express.js

After successfully deploying my application on Heroku and setting up file serving using Express Js, I encountered a CORS issue when sending requests from the frontend (Angular). This problem only occurs with the route for sending files. Below is the Expre ...

Differences in weekend start and end days vary across cultures

Looking for a solution to determine the weekend days per culture code in Typescript/Javascript? While most countries have weekends on Sat-Sun, there are exceptions like Mexico (only Sunday) and some middle-eastern countries (Fri-Sat). It would be helpful ...

What is the method for storing a JSON object path in a variable for use in a template?

Trying to fetch data from a lengthy path has proven challenging for me. I attempted to store the path in a variable and incorporate it into the template, but encountered some issues. Could someone assist me with this? Here is what I have tried: My store ...

Using Angular: A guide to setting individual values for select dropdowns with form controls

I am working on a project that involves organizing food items into categories. Each item has a corresponding table entry, with a field indicating which category it belongs to. The category is represented by a Guid but displayed in a user-friendly format. C ...

Exporting Javascript functions is not possible

Programming in TypeScript import { Component, OnInit } from '@angular/core'; import {loadCalendar} from '../../../../scripts/artist/artist-home'; import {activate_searchBar} from '../../../../scripts/search_bar_activate'; @C ...

Executing a PHP function within a Laravel controller using Ajax

In my Laravel project, I have a Controller named Clientecontroller that is working perfectly. It contains a method called listar() which retrieves client information. public function listar(Cliente $cliente) { $clientes = DB::table('clientes' ...

What is the most efficient way to retrieve 10,000 pieces of data in a single client-side request without experiencing any lag

Whenever I retrieve more than 10 thousand rows of raw data from the Database in a single GET request, the response takes a significant amount of time to reach the client side. Is there a method to send this data in smaller chunks to the client side? When ...

Learn how to implement icons within Textfield components using Material-UI and TypeScript in React

I have successfully created a form with validation using TypeScript Material UI and Formik. Now, I am looking to enhance the visual appeal by adding a material UI Icon within the textfield area. Below is a snippet of my code: import React from 'reac ...

Retrieving JSON data through HttpClient in Angular 7

I am attempting to retrieve information from this specific URL. The data obtained from this URL is in JSON format. This particular file is named data.services.ts: import { Injectable } from '@angular/core'; import { HttpClient } from '@an ...

The IDE is showing an error, but Jest is able to run it flawlessly

I recently created a Jest unit test for a TypeScript function called checkEmail, which internally uses showAlert. The showAlert function in the utils.ts file looks like this: export const showAlert = (message: string) => { toast(message); }; In my ...

Ways to verify whether any of the variables exceed 0

Is there a more concise way in Typescript to check if any of the variables are greater than 0? How can I refactor the code below for elegance and brevity? checkIfNonZero():boolean{ const a=0; const b=1; const c=0; const d=0; // Instead of ma ...

List the attributes that have different values

One of the functions I currently have incorporates lodash to compare two objects and determine if they are identical. private checkForChanges(): boolean { if (_.isEqual(this.definitionDetails, this.originalDetails) === true) { return false; ...

What is the best way to incorporate node modules into my tsconfig file?

I've taken some raw angular typescript components and packaged them into a private NPM module for sharing between various projects. Although I import these components like any other npm library, I encounter an error message when trying to serve my ap ...

What could be the reason for the exclusion of 'null' from the return type of Document.getElementById in VS Code?

Versions of VS Code: Experimenting with 'Type Narrowing' Code in VS Code has brought to my attention a discrepancy between the information provided by VS Code and TypeScript Playground: In VS Code, it shows that the return type of Document.getE ...

Streamline imports with Typescript

Is there a way to import modules with an alias? For example, I have an angular service in the folder common/services/logger/logger.ts. How can I make the import look like import {Logger} from "services" where "services" contains all my services? I've ...

The 'type' property within the NGRX Effect is not present in the type Observable<any[]>

I am currently in the process of upgrading my Angular app from version 6 to version 7. Additionally, I am upgrading the TypeScript version from 2.7.2 to 3.1.6. The issue I'm encountering is that TypeScript is flagging an error stating that my ngrx ef ...

The function e.preventDefault() appears to be ineffective when applied to both the submit button and anchor tag within an

In my ASP.Net Core MVC App View <form> <div class="container"> <div class="row"> <div class="col-md-offset-2 col-md-4"> <div class="form-group"> <input type="text" class="form-contr ...

Ensure that the dynamically inserted <title> tag remains intact in Angular even when the page is re

Can the dynamic title tag be preserved when the page is refreshed? When I refresh the page, the title tag reverts back to the original one specified in the index.html temporarily before switching back to the dynamically added one. I want the title tag to ...