All Blogs

Type-safe MatCellDef

Angular Material is probably the most popular UI Components Library in the Angular ecosystem. Amongst the components that Angular Material provides, MatTable is one of the most used ones. It looks good, is feature-packed, and works with different types of dataSource.

The Problem

Despite being a well-crafted component, MatTable runs into the same limitation as any other UI Library which is ng-template does not have strong-typed for its template variable. I am talking about this syntax here, which is pretty familiar to you if you’re using MatTable

<ng-template let-someVar></ng-template>
<!--someVar does not have any type information-->

To be able to provide top-of-the-line customizations to its consumers, MatTable does have to leverage the Dynamic Template system of Angular. Here’s a usage example of MatTable

<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
    <!--- Note that these columns can be defined in any order.
        The rendered columns are set as a property on the row definition" -->

    <!-- Position Column -->
    <ng-container matColumnDef="position">
        <th mat-header-cell *matHeaderCellDef>No.</th>
        <td mat-cell *matCellDef="let element">{{element.position}}</td>
    </ng-container>

    <!-- Name Column -->
    <ng-container matColumnDef="name">
        <th mat-header-cell *matHeaderCellDef>Name</th>
        <td mat-cell *matCellDef="let element">{{element.name}}</td>
    </ng-container>

    <!-- Weight Column -->
    <ng-container matColumnDef="weight">
        <th mat-header-cell *matHeaderCellDef>Weight</th>
        <td mat-cell *matCellDef="let element">{{element.weight}}</td>
    </ng-container>

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

    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

The above snippet is extracted straight from MatTable Documentations

These <td mat-cell *matCellDef="let element"></td> are horrible. It brings the Developer Experience down greatly because element has no type information whatsoever. Additionally, this is also one of the features that have been asked multiple times on the Angular repository

While the Angular team does want to provide this enhancement, the technical limitation seems to stem from how the Angular Compiler works, so it is a little low-level and will pose a great challenge for either the team, or the community to contribute.

So, as developers, what can we do in this case? Well, there is a workaround that you can leverage to improve your team’s DX if you’re using Angular Material, specifically MatTable, in your applications. That is the power called Directive.

The Solution

Before we get to the actual solution, we need to analyze what we know and what we need to do first: - dataSource is the piece of data that is going to provide the type for element - matCellDef cannot infer the type of its parent’s input. There is no way for matCellDef to be like “Ok, for this template variable, I want to use the type of this Input from my parent”. - matCellDef is the selector of MatCellDef directive and is a Structural Directive, which means there is some syntactic sugar that we can leverage.

The above are the things that we can analyze from the consumers’ usage of MatTable. Besides these points, there is one more thing that we need to know. In Angular low-level area (Compiler, Language Service, etc…), there are a couple of static flags that we can use to accommodate the Language Service like ngAcceptInputType, or ngTemplateContextGuard. The latter is the one we will leverage.

With those in mind, let’s get started with creating a Custom Directive that effectively monkey-patches the MatCellDef

import { CdkCellDef } from "@angular/cdk/table";
import { Directive, Input } from "@angular/core";
import { MatCellDef, MatTableDataSource } from "@angular/material/table";
import { Observable } from "rxjs";

@Directive({
    selector: "[matCellDef]", // same selector as MatCellDef
    providers: [{ provide: CdkCellDef, useExisting: TypeSafeMatCellDef }],
})
export class TypeSafeMatCellDef<T> extends MatCellDef {
    // leveraging syntactic-sugar syntax when we use *matCellDef
    @Input() matCellDefDataSource:
        | T[]
        | Observable<T[]>
        | MatTableDataSource<T>;

    // ngTemplateContextGuard flag to help with the Language Service
    static ngTemplateContextGuard<T>(
        dir: TypeSafeMatCellDef<T>,
        ctx: unknown
    ): ctx is { $implicit: T; index: number } {
        return true;
    }
}

Let’s walk over each piece in this Directive

The Setup

The Patch

All that is left to do is to declare our TypeSafeMatCellDef directive.

Before: element is of type any Before: element is of type any

After: element is of type PeriodicElement which is the type of our dataSource After: element is of type PeriodicElement which is the type of our dataSource

You can check out the Stackblitz below. However, it is hard to see the difference because of Stackblitz. You can try accessing an arbitrary property on element like element.fooBar, and you’ll see that the Compiler will fail to compile because fooBar is not a property of PeriodicElement.

Conclusion

Strongly-typed data is essential for a good and healthy Developer Experience. This has been proven by the risen popularity of TypeScript over the past couple of years. Although this solution works pretty well with not much custom code, I’m still looking forward to a more official solution from the Angular Team, and I am sure I’m not alone. Have fun and good luck 👋

Special thanks to @beeman_nl and @jefiozie for proofreading this blog post.

Published on Fri Mar 19 2021


Angular Material