Technology Blog Posts by SAP
cancel
Showing results for 
Search instead for 
Did you mean: 
AndreasKunz
Product and Topic Expert
Product and Topic Expert
1,341

One year ago, Martin Häuser blogged about the newly released JavaScript-to-TypeScript conversion feature of Joule in SAP Build. Now, with the new release 0.2.0, the UI5 MCP server has also received such a feature. Here I write about the experience while validating the feature and give hints about what to expect and how to get the most out of it.

This article is for UI5 developers familiar with TypeScript basics who are considering converting existing JavaScript applications. If you want to check out the basics of MCP servers before continuing, read our initial release blog post. For comprehensive UI5-specific TypeScript information see this page.

TL;DR: Scroll down to the end of this article when you are only interested in the learnings and hints, without all the technical details of getting there and tweaking the feature.

 

Why convert existing apps to TypeScript?

Much has been written about the value, efficiency, and developer happiness that comes with using TypeScript when writing UI5 applications. Don't take my word for it, just see this blog post for a real-world report of a team which migrated or this one for a similar opinion. Read how people "wouldn't go back for anything in the world" and "won't go back once you try it". Frankly, all feedback I heard was very positive.

I'm often asked whether all UI5 applications should be changed to TypeScript. While I definitely recommend it for new apps, I usually answered that the migration effort for an existing app only pays off when a certain amount of future development or maintenance effort is expected to be spent on it.

This equation has now shifted!

With the newest feature of the UI5 MCP server, UI5-specific TypeScript conversion assistance is now available to AI coding agents, which means a powerful boost for such an endeavor. As part of development, I tested it with the most complex UI5 app I had written myself (14 controllers, 7 custom controls). It had grown for almost eight years to a state which made me shy away from TypeScript conversion even though it was sorely needed. The result of using the MCP server: I literally told a colleague that I was getting goosebumps as it was plowing through the code of this app.

 

Experience from converting an app

The app

I have to start by saying that the app is not on GitHub and not open-source. It isn't suitable for end-users without personal support, and its setup requires experts. It's the kind of app you start writing just for yourself but turned out to be so useful that it was used to manage the speakers and sessions and agendas of the last seven UI5con conferences we organized as well as the sibling conferences reCAP and HANA Tech Con, the UI5cons in India and several "SAP Inside Track" events across Europe. So it's a productively used app that better doesn't break down.

You use it when you suggest session proposals (and the jury rates the proposals in this app), but at its heart, it looks like this for event admins:

AndreasKunz_0-1765544277502.png

And it comes with a drag&drop agenda planner developed as custom controls (not much of an agenda there right now, as no UI5con is currently planned):

AndreasKunz_1-1765544438959.png

When the time had come to give the new MCP server feature a real-life check and some fine-tuning, this app with its history and quirks was just the right choice.

Using the app to validate the MCP server tool

As preparation, I had an AI agent look at all files of the project and create an "AGENTS.md" file summing up its functionality and how things are working. This is not required specifically for TypeScript conversion, but probably useful anyway when an agent works with a project, as it gets an overview without having to first look around every time it starts a task. 

The prompt I then used to launch the conversion was:

@/AGENTS.md Convert the SAPUI5 UI of this project to TypeScript.

Use the MCP tool to get conversion guidelines. Ignore the tests for the time being.

By the way, I used Cline with Anthropic's Claude 4.5 Sonnet LLM, but it would work similarly with other LLMs and agents like GitHub Copilot, Cursor, Windsurf,... I explicitly mentioned the MCP tool and added AGENTS.md in the prompt as references. Probably that's not needed, but when starting a task that may run for an hour and may save days of work, then requesting precisely what I want doesn't hurt.

It started reasonably by calling the MCP tool and looking around in central project files like package.json, ui5.yaml and other top-level configuration files.

AndreasKunz_2-1765545373741.png

 

It then came up with a conversion plan—for the project setup in "phase 1", then for the concrete JS files.

AndreasKunz_0-1765548439531.png

AndreasKunz_1-1765548540598.png

These steps and this order make sense and this is no coincidence: of course the MCP tool suggests how to best proceed and what to do. The agent went through "phase 1" and "phase 2", then stopped with a summary of the achievements so far, so I asked it to proceed with the custom controls.

Starting with central reuse modules on which the other files depend (in terms of TypeScript: depend on their typed APIs) is what I also suggest to humans doing the conversion. A half-converted project works just fine like this and allows working in several steps instead of one big-bang conversion.

This message during the custom control conversion made me admire its coolness and understanding of the process:

AndreasKunz_5-1765546348935.png

Before converting the controllers, it again paused with a summary, but after a while, before the largest controller files, it explained that it could not continue like this due to a filled-up context window — oops!

AndreasKunz_6-1765546587017.png

This is something to keep in mind despite all the grace and wonders of AI agents: technically, they are still based on algorithms which predict the next part of a word from all the preceding words. The more conversation history there is, the more computationally complex (and expensive and to some degree slower) the response will be. Also: the fuller the context is, the less will this prediction be able to precisely take into account each single piece of the past conversation. Or in more human terms: it won't remember all details when it is flooded with too much unrelated stuff like contents of files which have already been converted. For sure, the AI coding agents apply clever measures to clean unneeded content from the context, but in general it makes sense to start from scratch when doing a different task, even when not being alerted like this. And in this case of one huge task the agent itself had to remind me that it could no longer digest all that input.

In such a case, start a new conversation. I copied 1.) the original prompt, followed by 2.) a hint that the conversion has already started and 3.) the conversion status up to this point. It was luckily provided by the agent, but if not: simply ask for it. The new conversation resumed smoothly from the point where the old one ended.

Iterating and refining the feature—getting better and better

Remember, this was not a TypeScript conversion to get the job done, but one for validating the MCP tool! The real work just started when the agent declared the job done. 

Result of the very first run:

  • The app immediately ran successfully, which was a good sign, but the tests wouldn't even start. This was due to the complex way how test data is supplied to the app. Can't blame the AI for that.
  • It used "as any" casts way too often (more than 100 times). When converting an app, it might be reasonable at some places to get the first result done, but doing it too often undermines the type safety.
  • It did dozens of casts like "(this.byId("form") as unknown as {setVisible: (v: boolean) => void}).setVisible(false);" where it would first cast to unknown, then to an object which had exactly the method that was called. Instead, it should have cast the retrieved control to either the base class sap.ui.core.Control when sufficient, or should have checked the XMLView to find out what type of control exactly will come back.
  • It declared some new types in the beginning of files, but did so in isolation: in one file it declared the parts of the type needed in this file, in the next file it again declared this type, but other parts of it. Instead of defining the type once and complete in a central location and then re-using it in all files.
  • In particular, it declared the jQuery type several times (it is directly used in the code of some controllers) instead of making sure the jQuery types are present from their own type definitions.

The conversion guidelines tool in the MCP server was then tweaked to prevent these issues and the process was re-started. The number of "any" casts for example was then drastically reduced and the strange "unknown"-plus-custom-type casts mentioned above were completely gone. Of course this tweaking was an iterative process.

In other iterations, it...

  • ...hallucinated a version of @ui5/ts-interface-generator that doesn't exist at all.
  • ...did not do "npm install" after adding the UI5 type definitions to package.json, but continued converting all the files saying that the type errors are expected and will go away when "npm install" is run (well, why don't you run it? Then you could verify whether the types really work!).
  • ...called the @ui5/ts-interface-generator in the root of the project despite having created an additional tsconfig.json in the "webapp" subfolder. This made the interface generator pick up the wrong tsconfig.json (the one for the server part of the application), so it didn't find any custom controls and didn't create the interfaces for them.

One of the fixes for these issues was adding code to the MCP tool which fetches the actual latest version of the @ui5/ts-interface-generator at tool invocation time (like it was already done for the other dependencies).

Despite all the tweaking of the tool, working with LLMs always means working with probabilities! As sad as it is, you cannot expect such a tool to work 100% as expected, or to report a bug when it doesn't and get this bug then analyzed, understood, and fixed for good. You might even encounter the above issues again when using the tool and there is NO way we could ensure it won't ever happen again.

In one iteration, the agent even got on a bad track right from the start. It set up the tsconfig.json file in a weirdly wrong way, resulting in the UI5 types not being found. They were installed, they were there and referenced, but neither the agent nor I, I have to admit, understood why they weren't applied. After seeing the agent trying to solve the problem in truly strange ways, I aborted this conversion attempt. Remember the thing about LLM contexts: when there is too much weird and wrong content in the conversation history which influences every single token it spits out as the process continues, then it is sometimes better to re-start cleanly.

Remaining errors

When the agent declares the conversion done, there are often a few dozen TypeScript errors left. Sometimes, those are errors which are NOT thrown by the TypeScript transpiler, but only displayed by the code editor (in my case Microsoft VS Code), as they sometimes use different settings (maybe due to the different tsconfig.json files for different project parts in my case).

You can ask the agent to work on those issues, but in my experience it will often not find good solutions for those, but rather take the "any" type shortcut. Those issues are worth to look at manually. Sometimes they actually reveal bugs in the app or the UI5 types!

  • One issue was a bug in the types of the sap.ui.core.XMLComposite control which I then fixed.
  • Others were caused by a bug in my custom control using the 'hidden: true' setting for making aggregations private, when the correct setting is 'visibility: "hidden"'.
  • Sometimes—despite tool improvements—the specific event types of UI5 are not used, e.g. here it used "Event" instead of  "Route$PatternMatchedEvent" from the "sap/ui/core/routing/Route" module AndreasKunz_2-1765551512101.png
  • But sometimes there are still application-dependent structures and property names left which are not in the UI5 type definitions and need their own custom type defined: AndreasKunz_3-1765551660878.png
  • It also depends on the eslint settings whether remaining "any" casts are considered as errors - you could change the settings or find better types.

How JavaScript-to-JavaScript comparison validates TypeScript

With large amounts of code converted and moved from a *.js to a *.ts file, it's hard for humans to review the result. You cannot easily diff them with a focus on actual logic—even though much of the code should still be the same. The structural changes are just too large.

But remember: in the end, the TypeScript code is transpiled to JavaScript during the build because this is what browsers execute! As TypeScript is basically just JavaScript plus types, the pure logic of the transpilation result should be the same as it was before. In addition, the structural changes of proprietary UI5 class and module definition syntax to ECMAScript modules and classes will also be reverted. Hence, ideally the transpiled TypeScript should look pretty much like the original JavaScript—at least close enough to spot potential conversion problems much easier in an interpretable diff.

So you can use any diff tool to compare the build result of the TypeScript app with the original JavaScript sources. In my case, 14 of 39 files in total had a difference. Most differences were trivial, like "var" converted to "const", "function"s converted to arrow functions, anonymous functions converted to named functions and "use strict" moved. Others introduced additional null checks or other changes related to the stricter typing. In some places the "_interopRequireDefault" function dealing with module exports was added.

One surprise was that the converted controls had also their control renderer migrated from the old string-based APIs like

renderer: function (oRm, oControl) {
oRm.write('<div');
oRm.writeControlData(oControl);
...
to renderer version 2 with semantic rendering:
renderer: {
apiVersion: 2,
render: function (oRm, oControl) {
oRm.openStart('div', oControl);
...
I hadn't asked for that, but won't complain, as the resulting renderer was correct.
Another functional difference was fixing the display of a regular expression in an error message. Nice, thanks!
But the most important result was that no change indicated broken or really changed functionality.
 

The actual value (and cost)

Despite of all this talk about errors and issues, the conversion is a process that takes maybe an hour with an AI agent (plus a few hours of manual polishing afterwards), but would have taken days when done manually. And potentially saves many days of work down the road as TypeScript greatly helps with development and maintenance efficiency.

Speaking of value, there is also monetary cost involved. A potential license fee of the AI agent tool itself might already include some or all of the AI tokens consumed by the conversion process. But to give an example for the imaginary isolated token cost, the conversion of this 14-controller-7-controls app consumed about 16 million input tokens (oops!) and (only!) 90k output tokens.

Using the current end-user API pricing of Anthropic ($3 per million input tokens and $15 per million output tokens) this sums up to about $50, which is likely not the biggest cost factor considering the invested (and saved!) working hours. Of course different models have different costs—and results. Google's Gemini 2.5 pro would only cost a bit more than $20, the still experimental Gemini 3.0 pro about $33, Anthropic's smaller Claude 4.5 Haiku less than $17, the bigger Claude 4.5 Opus more than $80.

Personally, I'd probably go for one of the best available models because saving $10 for the conversion may lead to having a less optimal codebase for years to come. The current top models can be seen in e.g. the LMArena leaderboard, so you can pick models with a good performance/price ratio. 

From trying different AI agents and LLMs in the past, I also have to say that they all have different "personalities" and approaches with regards to communicating and reading/changing files. One model may do all changes in a file in one go, another model I have seen doing a dozen single-line changes one-by-one, with a full (costly!) LLM round trip for each modification. Sometimes models are more paranoid, inspecting files again and again, looking around to make sure they are not missing anything. This means that an isolated look at the token prices does not give the complete picture.

 

Key Learnings and Best Practices

All in all, here’s how to get the most out of the UI5 MCP server’s TypeScript conversion feature:

Before You Start

Create project documentation - Have an AI agent generate an AGENTS.md file summarizing your app’s architecture (or write it yourself). This gives valuable context for the conversion and all future work.

Choose the best model - Don’t save $20-30 on a cheaper model when the result affects your codebase for years. Claude 4.5 Sonnet has worked well, Gemini 3.0 might even have a better performance/price ratio.

Commit everything - Ensure you can easily revert or compare changes if needed. Also commit before you do additional manual changes or the agent starts its next phase, so you can revert or re-check in isolation.

During Conversion

Trust the process - Let the agent follow the MCP tool’s suggested order: project setup → central modules → custom controls → controllers. But:

Don’t hesitate to restart - If the agent makes systematic mistakes (wrong tsconfig, excessive "any" casts), abort and restart cleanly with additional hints in your prompt if needed.  It’s faster than correcting a bad track when all the code has been touched. Or interrupt and adjust the guidance when you see a trend of minor issues.

Watch for context limits - When the agent signals a full context window (or when you feel/know/see it is), start fresh with: original prompt + note that conversion is in progress + current status summary.

After Initial Conversion

Expect remaining errors - Budget some hours for manual polishing. This is normal and valuable.

Fix errors properly:

  • Type errors sometimes reveal actual bugs—fix them, don’t suppress them. Or report to the UI5 team when the types are wrong.
  • Create central type definitions instead of scattering them across files. The tool tells the agent to do so, but probably you have the better oversight and can clean this part up.
  • Use specific UI5 event types (Route$PatternMatchedEvent) not generic Event. The tool tells the agent to do so, but the LLM often doesn't know all those types, so you are likely to see some of these issues remaining.
  • Excessive "any" casts defeat the purpose—replace with proper types.

The Bottom Line

Do not expect a perfect result! This tool (or rather the AI coding agent instructed by this tool) doesn’t produce perfect TypeScript, but it gives you a massive head start. A several-day manual conversion becomes an hour of agent work plus some hours of polishing with the benefits adding up afterwards day by day. That’s a big productivity gain.

The equation has changed: even legacy apps in maintenance mode can now benefit from TypeScript conversion. The barrier that once made me postpone converting my own 8-year-old app has been lowered dramatically.

What the tool does: Removes the tedious part of the work so you can focus on fixing meaningful type errors, creating proper domain types, and improving code quality where it matters.

What you still need: TypeScript knowledge and careful code review. The agent handles the tedious conversion; you handle the intelligent refinement.

If you have a JavaScript UI5 app that could benefit from TypeScript, there’s never been a better time to make the switch.

Try it yourself: The UI5 MCP server can be used with all AI coding assistants supporting MCP, is available via npm, and installation instructions and its code are at GitHub. Questions? Share your experience in the comments below or suggest improvements at GitHub.