Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
AndreasKunz
Advisor
Advisor
When using OData services in UI5 apps written in TypeScript, you can easily generate type definitions for the entities in the services. This blog post demonstrates how.

Prelude


It was after my UI5con 2022 appearance (in what was originally Wouter Lemaire's TypeScript presentation but then turned into a sort of all-star jam session also featuring Volker Buzek and Peter Müßig), when someone approached me with the idea to generate TypeScript types for the data structures in OData services.

It was one of the ideas which I on the one hand hadn't thought of so far, but which are on the other hand so obvious that others surely have not only thought about, but even implemented it. After all, OData services are well-described with metadata!

While writing this blog post, I found a local commit of mine from July 2022, which experimented with one such implementation named "odata2ts", but was then forgotten about. Good ideas keep coming back, though.

So a while later we looked for existing implementations and got in touch with Hubert Drecker, the author of said odata2ts. This resulted not only in interesting online co-hacking sessions to explore what else could be done on top, but also in his appearance in UI5ers live in April 2023.

Getting to the Point


When developing in TypeScript, all objects in the source code (should) have a well-defined type. For the UI5 APIs and things going in and out, these types are defined in the UI5 type definitions, but the data structures handled by applications are defined inside the service, in case of e.g. SAP CAP services in the *.cds files. Other OData implementations may have the structures originally defined in a different way, but ultimately, the service metadata provided at runtime is part of the OData standard and hence common to all OData implementations.

The Options - Available Tools


Looking for ways to generate TypeScript types from these application-specific data structures, we found two NodeJS-based tools, both under the MIT open source license, both available via npm, and both with several hundred commits and actively maintained for a long time:

  1. cds2types (GitHub😞 takes *.cds files as input and then outputs *.ts files. This means it can only be used for OData implementations based on CDS (like CAP). There is not a lot of documentation, but the usage is straightforward.

  2. odata2ts (GitHub; documentation😞 has three different levels of usage. In the most basic mode, it works similarly to cds2types: it takes OData metadata documents as input and then outputs *.d.ts files. But in addition, it offers a type-safe query builder that abstracts away the task of constructing an OData URL and in the most powerful mode, it acts as a complete Axios-based OData client.


[Edit: with @ottogroup/ui5-odata-generator there exists at least one more such generator. I hadn't found it before it was mentioned in the comments below, though. Would be worth to look into, as well.]

Comparison


As long as you are using regular UI5, the ODataModel takes over the tasks of the more powerful odata2ts modes, so let's compare the basic mode only:

The type output of both tools is very similar, but not fully identical (see concrete examples further down). The most significant differences are:

  1. odata2ts acknowledges that the overall data structure and the editable portion of it are different and generates two types for each entity.

  2. Date-related OData types are represented as string in odata2ts, but as Date in cds2types. While the latter sounds more correct at first glance, this does not fit the runtime (OData Dates are strings in UI5 app code) and indeed there is also an issue report in the cds2types repository about this. This difference could stem from the fact that cds2types was probably created for usage on the server side, where the dates might be dates, while odata2ts focuses on client-side usage.


Both differences made me focus on using odata2ts for an in-depth look.

Furthermore, odata2ts comes with a number of configuration options beyond the simple input/output parameters which cds2types also has. This can be considered as advantage (more features and flexibility) or disadvantage (more settings needed), depending on the exact needs.

The odata2ts OData Client Mode


When not using the full UI5 framework, but UI5 Web Components (no matter the actual framework used, e.g. React), then the OData client mode of odata2ts may be very handy as it takes over a lot of the tasks for which the ODataModel is responsible in UI5. But I'll leave a deeper exploration of this topic to others more familiar with both the UI5 Web Components and OData.

Making use of odata2ts in a real app


To get a hands-on impression of how it works, I used the typescript version of the UI5 CAP Event App sample and replaced the manually-written types for the "Person" object with generated ones. The result can be found here and is explained below.

This is how odata2ts can be used in a real-life UI5 app:

Step 1: Get the Service Metadata


The metadata XML file can either be downloaded manually from http://localhost:4004/event-registration/$metadata after starting the CAP server locally (yarn start:server) or the CDS tools can be used to directly generate the XML from the *.cds files without starting the server:
cds packages/server/srv/eventregistration-service.cds -2 edmx > packages/ui-form/src/model/event-registration-metadata.xml

Step 2: Add odata2ts as Dependency


As yarn is used in this project and only the packages/ui5-form sub-project is a UI5 freestyle TypeScript app, the dependency to odata2ts is added like this:
cd packages/ui-form
yarn add --dev @odata2ts/odata2ts

Step 3: Configure odata2ts


A specific configuration file (packages/ui-form/odata2ts.config.ts in this app) is needed for the configuration of odata2ts:
import { ConfigFileOptions, EmitModes, Modes } from "@odata2ts/odata2ts";

const config: ConfigFileOptions = {
mode: Modes.models,
emitMode: EmitModes.dts,
services: {
eventRegistration: {
source: "src/model/event-registration-metadata.xml",
output: "gen",
propertiesByName: [
// list of managed fields which are not editable from the user's perspective
...["createdAt", "createdBy", "modifiedAt", "modifiedBy", "Email"].map(
(prop) => ({ name: prop, managed: true })
)
]
}
}
}

export default config;


  • mode: the  models mode is the one used to only get the type definitions generated.

  • emitMode: dts only generates *.d.ts files, no code (no code is needed for the pure types).

  • services: here, the service is configured for which the type definitions are generated:

    • source points to the metadata xml file saved before

    • output says where the type definitions should go ("gen" as sibling of "src")

    • propertiesByName is used here to declare certain system properties as "managed". This means they are not "normal" data properties which the user can change, but set by the system. This will be useful in the controller code when looping over the user-entered properties.




A word of caution: the target directory contents are deleted, so make sure to pick a new directory and not one that has content you want to keep!

Step 4: Run it!


You can now trigger generation in the "ui-form" directory by executing:




npx odata2ts


As result, the file packages/ui-form/gen/EventRegistrationServiceModel.d.ts will be generated, containing types for the entities like "Person" and "FamilyMember". To keep it short, only the Person is shown here:



export interface Person {
createdAt: string | null;
createdBy: string | null;
modifiedAt: string | null;
modifiedBy: string | null;
ID: string;
FirstName: string | null;
LastName: string | null;
Email: string | null;
Birthday: string | null;
FamilyMembers?: Array<FamilyMember>;
}
export declare type PersonId = string | {
ID: string;
};
export interface EditablePerson extends Partial<Pick<Person, "FirstName" | "LastName" | "Birthday">> {
}

Here you see how the "Person" type has all the properties which are present when a person object is loaded from the service, but the "EditablePerson" only has those properties which can actually be set by the user.

Step 5: Use the Types


The manually-defined types in the controller (Person, PersonProp and Employee) can now be deleted. As a result, there are some errors in the validateData() method where these types had been used before:


Code including the errors shown after deleting the types


Error 1 can be easily fixed by changing Employee to Person - one of the manually written types just had a different name.

Error 2 is shown because the newly created object is actually not a Person: on the one hand several properties are missing, only the editable properties are present. So a way to get rid of the error would be to use the type EditablePerson here.

But actually, this would not be correct, as the values are not names and a birth date. Instead it's the localized terms "birthday" etc. So this is rather a map where the keys are the editable properties of a Person and the values are strings to be shown in an error message (when the respective property has not been filled by the user). In TypeScript you can enumerate the properties of a type into a new type with keyof and this is exactly what we use here for the map keys: the correct type is: Record<keyof EditablePerson, string>

Error 3 is again due to a no longer existing type. What is meant here is a type that contains all editable properties of a Person. Hence, we can use keyof EditablePerson again.

Error 4 was so scared of our Error-annihilation skills that it simply went away as we got closer. Oh, and "fields" and "prop" are both no longer implicitly any after the above changes. This may have contributed, too.

Done!


All errors are gone, we have an app whose types related to data objects are fully generated from the service and could be easily updated in case of service changes.

As mentioned above, you can find the result along with a similar explanation in the "typescript-odata2ts" branch of the ui5-cap-event-app sample project at GitHub.

Conclusion


Some applications may just bind data to the UI, so the UI5 OData models would take over every task related to data types and the UI5 application code would not even need to handle the data structures which are displayed or edited.
But for other applications some direct handling of that data is required. While the relatively simple types in the sample app could easily be written manually, real-life apps may have more complex data structures, which in addition might even change during development.
This is where the tools we have seen come in very handy and help you getting even more benefits from TypeScript in UI5 apps.

 

 
3 Comments