AG Grid: Typescript Generics

Stephen Cooper - Jul 14 '22 - - Dev Community

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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.

Row Data enforces ICar interface

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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!

Compile time error for incorrect property name

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.

Auto completion of data properties based on generic type

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>


Enter fullscreen mode Exit fullscreen mode

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.

React validating generic types are consistent

Angular

For Angular, providing a type to your rowData property is enough to enable generics for the entire component.



 public rowData: ICar[];


Enter fullscreen mode Exit fullscreen mode


 <ag-grid-angular
      [columnDefs]="columnDefs"
      [rowData]="rowData"
      (gridReady)="onGridReady($event)"
    ></ag-grid-angular>


Enter fullscreen mode Exit fullscreen mode

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>) { }


Enter fullscreen mode Exit fullscreen mode

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.

Angular validating generic types are consistent

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


Enter fullscreen mode Exit fullscreen mode

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.

params.data 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;
    }
};


Enter fullscreen mode Exit fullscreen mode

While it is possible to silence the "Object is possibly 'undefined'" errors with a non-null operator !. such as params.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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .