My first experience with TypeScript in UI5 – ODataModel Service Wrapper

Around April SAP announced for the first time TypeScript Support for UI5. Since then, I’ve tried to use this in every new UI5 project where possible. Now it is time to share my experience with TypeScript in UI5 in this blog post series.

In the previous blog post I added a BaseController to the earlier generated UI5 TypeScript project using the easy-ui5 generator.

In this one, I’ll take you back to a blog post which I shared a few years ago. A blog post about a wrapper that allows you to use the UI5 ODataModel v2 with promises. Maybe you still remember or even use this, if not I advise you to have a look at this blog post from the past: https://blogs.sap.com/2018/09/17/ui5-odata-requests-with-promises/

With TypeScript supported by UI5, I present you a newly improved TypeScript version of this UI5 ODataModel Wrapper!

My motivation to use a wrapper is still the same as in the first blog post. I try to use OData bindings as much as possible. Nevertheless, the limitations of the ODataModel v2 are forcing me to do manual requests. The v4 version is even more boxed into the framework… That in combination with SAP backend systems that have golden function modules that expect the data in a certain structure. Delivering an app that has great performance and can work with those function modules makes you do things different than bindings expect you to. That’s just what it is…

If you face similar problems and you like to use TypeScript, you might like to use this wrapper!

I provided two videos where I explain in more details the TypeScript BaseService and how I recommend to consume it:

1) TypeScript BaseService

2) How to consume this TypeScript BaseService

If you prefer reading, you can continue reading this blog post.

I put this wrapper in a TypeScript file which I call “BaseService.ts” in a folder “service”. You can put this in a library and consume it from multiple apps. At the moment there is no TypeScript support for libraries so I add it to the project itself:

Inside this BaseService it all comes down to this wrapper function, you might recognize it from the JavaScript version. With TypeScript, it is not only wrapping the ODataModel functions in promises, it also returns it as a type by using generic types. This allows developers to use the wrapper and provide the type of the result from the OData request.

public odata(url: string) { const core = { ajax: <T>(type: odataMethods, url: string, parameters: parameters<T>, data?: T): Promise<response<T>> => { const promise = new Promise<response<T>>((resolve, reject) => { let params: parameters<T>={}; if (parameters) { params = parameters; } params.success = (result: T, response): void => { const responseResult: response<T> = { data: result, response: response }; resolve(responseResult); }; params.error = function (error: unknown) { reject(error); }; if(data){ this.model[type](url,data,params); }else{ this.model[type](url,params); } }); return promise; } }; return { get: <T>(params?: parameters<T>): Promise<response<T>> => core.ajax('read', url, params), post: <T>(data: T, params?: parameters<T>): Promise<response<T>> => core.ajax('create', url, params, data), put: <T>(data: T, params?: parameters<T>): Promise<response<T>> => core.ajax('update', url, params, data), delete: <T>(params?: parameters<T>): Promise<response<T>> => core.ajax('remove', url, params) };
}

Full code: https://github.com/lemaiwo/TypeScriptServiceDemoApp/blob/main/src/service/BaseService.ts

The most elegant way to use this BaseService is by extending from it and creating a new more specified service object. In the case of this example I created a “NorthwindService.ts” file. This will be an object with all operations for the Northwind OData service that uses the wrapper function in the “BaseService”. This allows you to have service objects for every OData service used by your app and separate the definitions of the requests.

In this example I provide the following functions:

  • getSuppliersData: Simply triggers a get function that will return a list of Suppliers.
    • As you might have noticed, I also pas a generic type “Array<SuppliersEntity>”. This will provide us autocompletion on the result of this function.
  • getSuppliers: almost exactly the same to the previous function but now returning the full response object instead of only the data
  • getSuppliersWithFilter: this is an example how to pass parameters like you can also do when using the “read” function of the OData model.
  • getSupplierById: example on how to get a single record
  • getSupplierNextID: showing how to pass a sorter and other urlParameters
  • createSupplier: a simple POST request assuming the type you send will be the same you will return.

All types related to this service are defined at the top of the “NorthwindService” object.

Notice that compared to the JavaScript version, the TypeScript version is not returning an instance of the NorthwindService. The reason of this change is to allow extending this service object.

export default class NorthwindService extends BaseService { constructor(model: ODataModel) { super(model); } public async getSuppliersData(){ const result = await this.odata("/Suppliers").get<SuppliersEntitySet>(); return result.data.results; } public getSuppliers(){ return this.odata("/Suppliers").get<SuppliersEntitySet>(); } public getSuppliersWithFilter(filters:Array<Filter>) { return this.odata("/Suppliers").get<SuppliersEntitySet>({filters:filters}); } public getSupplierById(id:number) { const supplierPath = this.model.createKey("/Suppliers", { ID: id }); return this.odata(supplierPath).get<SuppliersEntity>(); } public async getSupplierNextID(){ var mParameters = { sorters:[new Sorter("ID",true)], urlParameters:"$top=1" }; const response = await this.odata("/Suppliers").get<SuppliersEntitySet>({sorters:[new Sorter("ID",true)],urlParameters:{"$top":"1"}}); return response.data && response.data.results.length > 0 ? response.data.results[0].ID + 1:0; } public createSupplier(supplier:SuppliersEntity){ return this.odata("/Suppliers").post<SuppliersEntity>(supplier); } public updateSupplier(supplier:SuppliersEntity){ const supplierPath = this.model.createKey("/Suppliers", { ID: supplier.ID }); return this.odata(supplierPath).put<SuppliersEntity>(supplier); } };

Full code: https://github.com/lemaiwo/TypeScriptServiceDemoApp/blob/main/src/service/NorthwindService.ts

Time to consume the NorthwindService! As this does not return an instance, we still have to instantiate the service. Normally I do this in the Component so you can access it in every controller. For demo purpose, I simply initialize the service in the onInit of the App controller:

The NorthwindService can now be used in the controller as promises (with async/await) using TypeScript. This gives you autocompletion and a super nice developer experience!

Another example on how to use this TypeScript wrapper for creating a new supplier:

Full project: https://github.com/lemaiwo/TypeScriptServiceDemoApp