In this article, we will show you how to make the most of Typescript Generics in AG Grid v28. We will demonstrate the great developer experience it unlocks with the help of code examples.
AG Grid Generic Types
There are two generic types that you can pass to AG Grid interfaces. They are: TData
and TValue
.
TData - Row Data Type
TData
is used to represent the shape of row data items. This generic parameter should match the interface used when defining your row data. If an AG Grid interface has a TData
generic parameter this will always refer to the row data type.
interface GridOptions<TData = any>{
rowData: TData[] : null;
}
To maintain backwards compatibility a default type of any
is provided. This mimics the behaviour of previous versions of AG Grid that explicitly type the rowData as any[]
.
TValue - Cell Value Type
TValue
is used to represent the type of a specific cell value. This generic type is more limited in scope and can be used within cell renderers/value getters when you want to give a type to the value
property.
export interface GetQuickFilterTextParams<TData = any, TValue = any> {
/** Value for the cell. */
value: TValue;
}
As with TData
, we also default TValue
to any
to maintain backwards compatibility.
Using Generic Types
In the examples below we use the ICar
interface to represent row data.
// Row Data interface
interface ICar {
make: string;
model: string;
price: number;
}
var rowData: ICar[] = [
{ make: "Toyota", model: "Celica", price: 35000 },
{ make: "Porsche", model: "Boxster", price: 72000 }
];
The best place to assign ICar
to the TData
generic parameter is via the GridOptions
interface. This will cascade the generic type down the interface hierarchy.
// Pass ICar to GridOptions as a generic
const gridOptions: GridOptions<ICar> = {
// rowData is typed as ICar[]
rowData: [ { make: 'Ford', model: 'Galaxy', price: 200 } ]
}
With the generic parameter set to ICar
, the rowData
property has the type ICar[]
instead of the default any[]
. This means that if you mistype one of the properties you will get a compile-time error.
It is worth noting that it is not just rowData
where the interface is used. For example, in the getRowId
callback, the params.data
property is typed as ICar
instead of any
. This is a result of AG Grid cascading the generic type down from GridOptions
.
This also applies to all the grid events that contain a data property. i.e onRowSelected
.
const gridOptions: GridOptions<ICar> = {
// Callback with params type: GetRowIdParams<ICar>
getRowId: (params) => {
// params.data : ICar
return params.data.make + params.data.model;
},
// Event with type: RowSelectedEvent<ICar>
onRowSelected: (event) => {
// event.data: ICar | undefined
if (event.data) {
const price = event.data.price;
}
}
}
If you use the grid api via gridOptions.api
or callback params.api
properties, then the api will be aware of the row data type too. This means that the method api.getSelectedRows()
will return rows as ICar[]
.
// Grid Api methods use ICar interface
function onSelection() {
const cars: ICar[] = gridOptions.api!.getSelectedRows();
}
Configure via Individual Interfaces
While it is possible to configure everything inline within a gridOptions
object, you may want to split things out for readability / re-usability. To benefit from Typescript generics we can provide our ICar
interface to any AG Grid interface that accepts a TData
generic parameter.
For example, we could configure our event handler with
RowSelectedEvent<ICar>
. This results in the event.data
property being typed as ICar | undefined
. (See Generic Type or Undefined below for more details on the additional undefined
type).
function onRowSelected(event: RowSelectedEvent<ICar>) {
if (event.data) {
// event.data: ICar | undefined
const price = event.data.price;
}
}
Advantages over any
There are two main reasons to provide generic types to AG Grid:
- Compile-time errors
- Auto-completion in your IDE
Compile Time Errors
Say we mistyped price
as prive
. Without generics, your application code would compile but your grid would no longer show prices in the grid. You would then have to track down the source of the bug. It could even be possible that this type of bug escapes into production if it slips past your tests.
However, if you supply generic types your application would not compile and would tell you the exact location of the bug. This enables you to instantly correct the code, instead of having to track down the bug. It will also be underlined with a red squiggle in your IDE!
Auto-Completion
As Typescript knows that our data
object conforms to the ICar
interface our IDE can suggest properties to us. This speeds up your development process as you do not have to spend time thinking about property names as they are suggested to you automatically.
Framework Specific Demo
React
If you are using AG Grid via the React component then you can pass your row data type to the AgGridReact
component via this syntax: <AgGridReact<ICar>
.
This will validate that all props
conform to the row data type.
<AgGridReact<ICar>
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
onGridReady={onGridReady}
></AgGridReact>
If you provide conflicting types you will get a compile-time error. For example, here we have provided a different generic type, INotCar
to the onGridReady
event which does not match our rowData
of ICar
.
Angular
For Angular, providing a type to your rowData
property is enough to enable generics for the entire component.
public rowData: ICar[];
<ag-grid-angular
[columnDefs]="columnDefs"
[rowData]="rowData"
(gridReady)="onGridReady($event)"
></ag-grid-angular>
If you are not using the rowData
property, then type any other input/output that takes the generic parameter. For example, setting the following type on the onGridReady
event would set the generic type for the entire component.
onGridReady(event: GridReadyEvent<ICar>) { }
If you provide conflicting types you will get a compile-time error. For example, here we have provided a different generic type, INotCar
to the onGridReady
event which does not match our rowData
type of ICar
.
Generic Type or Undefined
One thing you may notice when adding generic support to an existing application is that you may get errors relating to the data
property potentially being undefined
. You will not have seen this previously as the data
property was typed as any
which silently includes undefined
. By specifying the type explicitly, AG Grid has the opportunity to warn you, via typings, that the data property could be undefined
under some circumstances.
To demonstrate this we will look at a common pitfall that users have when adding custom cell renderers while supporting row grouping.
Common Pitfall with Cell Renderers
When writing a custom cell renderer you are likely to access the data
property from the ICellRendererParams
interface. Your first attempt may have code that accesses data properties with no undefined checks.
init(params: ICellRendererParams) => {
this.total = params.data.gold + params.data.silver + params.data.bronze;
};
This may work in your application but it is not technically correct or future-proof.
If you have row grouping enabled then as soon as the user groups by a column your cell renderer will fail. This is because when grouping the data
property is undefined
.
However, if you provide a generic type to ICellRendererParams
, such as IOlympicData
, your code will warn that the data
property can be undefined
.
With the generic type supplied you are more likely to correctly write the cell renderer to handle this case.
init(params: ICellRendererParams<IOlympicData>) => {
if(params.data){
this.total = params.data.gold + params.data.silver + params.data.bronze;
}else{
this.total = undefined;
}
};
While it is possible to silence the "Object is possibly 'undefined'" errors with a non-null operator
!.
such asparams.data!.gold
, we would recommend writing your code defensively in line with the AG Grid interfaces.
Conclusion
If you are using Typescript then we would encourage you to take advantage of our generic types to give you:
- Compile-time errors
- Auto-completion in your IDE
- Improved code typings
It is a small change in your type definitions that results in a greatly improved developer experience!
For more details visit our Typescript Generics documentation.