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
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
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
- https://github.com/angular/components/issues/22290
- https://github.com/angular/components/issues/16273
- https://github.com/angular/angular/issues/28731
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
Let’s walk over each piece in this Directive
The Setup
- We make
TypeSafeMatCellDef
to accept a type parameter<T>
. selector: '[matCellDef]'
We use the sameselector
asMatCellDef
. This is our monkey-patch{ provide: CdkCellDef, useExisting: TypeSafeMatCellDef }
Under the hood,MatTable
is leveraging Angular CDK Table and is providingMatCellDef
forCdkCellDef
token. Here, we provide ourTypeSafeMatCellDef
for that same token soMatTable
continues to work as expected.extends MatCellDef
We want our Directive to inherit the behavior ofMatCellDef
The Patch
@Input() matCellDefDataSource: T[] | Observable<T[]> | MatTableDataSource<T>;
Although we do make our Directive to accept a type parameter<T>
, we will never actually provide the type for the Directive directly since the instantiation of directives (or most building blocks in Angular) is Angular responsibility. ThisInput
acts purely as a way for us to pass in the type information. Another thing to note here is that the name of the@Input()
, we leverage another hidden feature of Angular.- If we name the Inputs of a Structure Directive prefixed with the selector, we can use the short-hand syntax when we use this Directive
trackBy
here is actually@Input() ngForOfTrackBy
ngTemplateContextGuard
is a TypeScript Type Guard. Here, we essentially say “ctx is of this type. Trust me TypeScript”
All that is left to do is to declare our TypeSafeMatCellDef
directive.
Before: element
is of type any
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.