Trusted answers to developer questions

How to add a layer of typing on top of third-party libraries

Get Started With Machine Learning

Learn the fundamentals of Machine Learning with this free course. Future-proof your career by adding ML skills to your toolkit — or prepare to land a job in AI or Data Science.

You may have noticed that the typing provided by third-party libraries often feels very loose. This is to maintain flexibility for all users, which means it is not possible to provide stricter forms of typing.

However, within our own applications, there is nothing stopping us from adding a layer of more explicit typing on top of the library interface. We can leverage Typescript to greatly improve the developer experience and encourage consistency in how the library is used.

To demonstrate the benefits of this approach, we will work through an example of adding type safety to AG Grid column types.

We will show how adding a level of explicit types prevents bugs and speeds up development.

AG Grid: Typing column types

A foundational concept of AG Grid is the column definitions. Column definitions govern all aspects of how a column appears, behaves, and interacts with the grid. Here’s an example of three columns defined for the grid with different behavior:

const gridOptions = {
  // define grid columns
  columnDefs: [
    { headerName: 'Athlete', field: 'athlete', rowGroup: true },
    { headerName: 'Sport', field: 'sport', filter: false },
    { headerName: 'Age', field: 'age', sortable:false },
  ],
  // other grid options ...
}

As we often want to apply similar sets of behavior to a column, we can set up predefined columnTypes to avoid explicitly repeating all the required properties. A column type is an object containing properties that other column definitions can inherit. Once column types are defined, we can apply them by referencing their name.

So, for example, we could define the following types as per the AG Grid documentation:

this.columnTypes = {
  nonEditableColumn: { editable: false },
  dateColumn: { 
    filter: 'agDateColumnFilter',
    filterParams: { comparator: myDateComparator },
    suppressMenu: true,
  },
};

These types would be used as follows:

this.columnDefs: ColDef[] = [
  { field: 'favouriteDate', type: 'dateColumn' },
  { field: 'restrictedDate', type: ['dateColumn', 'nonEditableColumn'] }
];
<ag-grid-angular
  [columnDefs]="columnDefs"
  [columnTypes]="columnTypes" />
</ag-grid-angular>

When working with projects that contain many grids, you may find that you are repeatedly setting up common column definitions. To avoid repeating code, you will want to start extracting these into column types. You can quickly end up with a large number of column types for many different column scenarios. This is when you might run into the following problems.

Potential issues

1) Sharing knowledge of existing and new column types

When you have a team working on a shared project, it can be difficult to share the knowledge and existence of custom column types, as they would have to be documented or at least visible via a shared code file. However, it is easy to forget about the existence of these types and miss any new additions, and relying on developers to check the contents of a shared file is not a great experience and could potentially lead to developers reinventing the wheel.

2) How do you catch typos in type names that will break your grid?

It is very easy to mistype a name when setting up a column definition (as any string is valid). The application will build successfully, but a core feature of the grid might be broken. Without careful code reviews or another testing process, this bug could slip into production.

Solving potential issues

We can solve both of the issues mentioned above by layering a Typescript interface on top of the AG Grid ColDef. Currently, the type property of ColDef is defined as string | string[]. One potential implementation is to extend the ColDef interface, but restrict the type property to a new union type of SupportedColTypes.

type SupportedColTypes = 'dateColumn' | 'nonEditableColumn';

interface AppColDef extends ColDef {
  type?: SupportedColTypes | SupportedColTypes[];
}

SupportedColTypes will define all possible column types that we support. Then, when defining our column definitions in our app, we will replace ColDef with AppColDef.

this.columnDefs: AppColDef[] = [
  { field: 'favouriteDate', type: 'dateColumn' },
  { field: 'restrictedDate', type: ['dateColumn', 'nonEditableColumn'] }
];

With this change, we solve the two issues we had above. To solve the first issue, there is now a defined list of types so that our IDE’s can leverage Typescript and provide auto-completion. This means that every developer has the complete list of supported types at their fingertips while setting up the column definitions, i.e., no context switching to another file to lookup column types anymore. Additionally, if a developer adds a new column type, as long as it is included in SupportedColTypes, it will be easily discoverable by the rest of the team.

IDE providing auto completion

To solve the second issue, we turned typos in column types into compile errors. This is hugely important as we can instantly correct mistakes during dev time and not in production!

Typos in column types turned to compiler errors

Implementing SupportedColTypes safely

It is now critical that SupportedColTypes remains consistent with the actual implementations of the shared columnTypes we have defined. We can leverage Typescript to ensure this is the case by using Mapped Types.

APP_COL_TYPES: { [key in SupportedColTypes]: ColDef };

Now, if you add another column type to APP_COL_TYPES, the compiler will complain if the key name is missing from SupportedColTypes as every property in APP_COL_TYPES has to be a key of SupportedColTypes.

Conclusion

As we have seen, even for libraries that provide Typescript interfaces, there is scope to add our own types on top to great effect.

RELATED TAGS

general
Did you find this helpful?