Type Information in a Structural Directive

A problem of generic structural directive is that we loose the type information. Unlike our typescript code we can not pass the generic type info in the html template. This article discusses an approach how to keep the type information.

Alexander Zarges
Dec 4, 2020 · 5 min read

In my last blog article I wrote an introduction to structural directives. This blogpost builds upon the knowledge we learned there.

In a structural directive we can receive content in order to use it in our projected template.

<div *appFoo="let bar">{{bar}}</div>

The structural directive would provide the context and by using $implicit the content becomes available. In this case it is a hard coded string ‘Hello World’.

@Directive({selector: 'appFoo'})
export class FooDirective implements OnInit {
constructor(private viewContainer: ViewContainerRef,
private templateRef: TemplateRef<{$implicit: string}>{
}

ngOnInit(){
this.viewContainer
.createEmbeddedView(
this.templateRef,
{$implicit: 'Hello World'})
}
}

TemplateRefis a generic class. The generic type that is defined there specifies the context that is passed into the template later when createEmbeddedView(templateRef, context) is called. In this case we specify that $implicitis of type string.

Of course, this not only works with primitive types but also with more complex let’s say an interface IPersonwith a name and an age.

export interface IPerson{
name: string;
age: string;
}
@Directive({selector: 'appFoo'})
export class FooDirective implements OnInit {
constructor(private viewContainer: ViewContainerRef,
private templateRef: TemplateRef<{$implicit: IPerson}>{}
...
}
IDE can extract type information of the interface IPerson correctly

However, this information gets lost when working with a generic structural directive.

Type information in a generic structural directive

Ok, so let’s think about a use case for a generic structural directive. We get a task from product management to provide a list of users where only the first 5 users are visible and all the others shall be hidden behind a Show More button. As we realise that this functionality could be used for other entity types as well , we decide to build a re-usable list-component. In order to stay flexible how each item from the list shall be displayed we provide a structural directive as a blueprint for the item.

We provide a generic list-componentwhich accepts an array of generic items (TItem)and a limit how many items shall be visible. If the limit is exceeded a Show More button is shown. On press all items are shown. Our structural directive will call setItemTemplate in order to provide the template to be used.

The directive itself is only injecting list-component and registers its templateRefon it.

The result looks like this

Loosing the type info

Unlike our typescript code there is no way of setting our generic type in the template.

const listItemDirective = new ListItemDirective<IUser>()

Thus, our uservariable that we pass into the structural directive has no information of its type. Instead, our IDE just shows user is from type TItem. Also it marks firstName and lastName as invalid because the properties do not exist on the the generic TItem.

Getting back the type info

Fortunately, there is a way how we can get back the type info for the user variable. It feels a bit hacky but it does its job.

We extend our ListItemDirectivewith a new input property called useTypeFrom from type TItem.

We can now set the type in the template by setting it with useTypeFrom .

getUsers()[0] returns the first user from the array which is from type IUser . By setting it on useTypeFromit gets the first user but also the type info of the user. Also, the actual return value can be null, so we could also provide a method that returns nullbut where the return type is set to IUser

getType(): IUser{
return null;
}
<ng-container *appListItem="let user useTypeFrom getType()">
</ng-container>

The typescript compiler now gets the info from which actual type TItemis and therefor all generic types are resolved with it and let user is now from type IUser. Also our IDE can now successfully parse the actual type of the user variable.

Conclusion

Having the type info also in the template helps the IDE to provide auto complete and also to prevent bugs due to typos. If you set the angular template checking to strict you also get errors during the compile time if you have a typo in your template. However, this only works if the type info is available.

You may think the list example from above is a bit constructed but a real world scenario where you have this problem is the table from material design. There you specify a column also with a generic structural directive and specify element.

<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef> Symbol </th>
<td mat-cell *matCellDef="let element"> {{element.symbol}} </td>
</ng-container>

Yet, let elementdoes not have any type info and could be anything. It would be really helpful to have the type info there as well

loosing the type in matTable

The solution to provide the type by providing a new input attribute on our structural directiveuseTypeFrom might feel a bit hacky but at least we get the typeinfo back.

The Startup

Get smarter at building your thing. Join The Startup’s +737K followers.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +737K followers.

Alexander Zarges

Written by

I'm a PWA developer with 7+ years of Angular experience. During the day I develop the WebApp for an IOT business application, during the night I work on aux.app

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +737K followers.