| name | ui5-typescript-conversion |
| description | A skill for converting UI5 (SAPUI5/OpenUI5) projects to TypeScript. |
UI5 TypeScript Conversion Guidelines
How to convert a UI5 (SAPUI5/OpenUI5) project to TypeScript: general rules, project setup changes, code conversion, and test conversion (separate file).
General Conversion Rules
Preserve ALL comments
You MUST preserve existing JSDoc, documentation and comments - never remove JSDoc or comments during the conversion. When converting to a class, add @namespace but keep ALL existing JSDoc.
Before:
return Controller.extend("com.myorg.myapp.controller.BaseController", {
getOwnerComponent: function () {
return Controller.prototype.getOwnerComponent.call(this);
},
});
After:
export default class BaseController extends Controller {
public getOwnerComponent(): UIComponent {
return super.getOwnerComponent() as UIComponent;
}
}
Be diligent
Carefully respect all guidelines in this document. Before each conversion step, consider all relevant details.
Go step-by-step
Convert step by step: TypeScript project setup first, then central files other files depend on, so typed versions are available for consumers. "allowJs": true in tsconfig.json allows semi-converted projects.
Avoid any type
Find the proper type or create an interface instead of any:
(this.getOwnerComponent() as AppComponent).getContentDensityClass()
Avoid unknown casts
Import and use actual UI5 control types. Inspect the XMLView to find which control type you get from this.byId(...). Use specific event types like Route$PatternMatchedEvent.
import SimpleForm from "sap/ui/layout/form/SimpleForm";
(this.byId("form") as SimpleForm).setVisible(false);
Create shared type definitions
Create shared types in a central location like src/types/.
Project Setup Conversion
1. package.json
Add the following dev dependencies if not already present:
{{dependencies}}
Do not increase existing major versions. Do not remove existing dependencies.
IMPORTANT: Also add @sapui5/types (or @openui5/types) matching the UI5 project version as dev dependency. Framework type and version from ui5.yaml or get_project_info MCP tool.
If dependencies changed, ensure npm install / yarn install is run. The typescript-eslint dependency is only relevant when the project already has eslint. Also add "ts-typecheck": "tsc --noEmit" script to package.json.
2. tsconfig.json
Add a tsconfig.json. Use this as reference, adapt paths to the project:
{
"compilerOptions": {
"target": "es2023",
"module": "es2022",
"moduleResolution": "node",
"skipLibCheck": true,
"allowJs": true,
"strict": true,
"strictNullChecks": false,
"strictPropertyInitialization": false,
"outDir": "./dist",
"rootDir": "./webapp",
"types": ["@sapui5/types", "@types/jquery", "@types/qunit"],
3. ui5.yaml
Add ui5-tooling-transpile-task and ui5-tooling-transpile-middleware:
builder:
customTasks:
- name: ui5-tooling-transpile-task
afterTask: replaceVersion
server:
customMiddleware:
- name: ui5-tooling-transpile-middleware
afterMiddleware: compression
- name: ui5-middleware-livereload
afterMiddleware: compression
Avoid duplicate entries — add to existing server/builder sections if they exist.
4. Eslint configuration
Only when eslint is already set up, enhance it with TypeScript-specific parts. Example eslint v9 eslint.config.mjs:
import eslint from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
globals: {
...globals.browser,
sap: "readonly"
},
ecmaVersion: 2023,
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname
}
}
},
{
ignores: ["eslint.config.mjs"]
}
);
Application Code Conversion
Step 1: Change UI5 class syntax to ES class syntax
Convert SuperClass.extend(...) to a standard class. Properties in the config object (second extend parameter) become class members. Annotate the class with @namespace in a JSDoc comment (it must immediately precede the class declaration) — the namespace is the part of the full name (first extend parameter) that precedes the class name.
Before:
var App = Controller.extend("ui5tssampleapp.controller.App", {
onInit: function _onInit() {
this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
}
});
After:
export default class App extends Controller {
public onInit(): void {
this.getView().addStyleClass((this.getOwnerComponent() as AppComponent).getContentDensityClass());
}
}
Step 2: Change to ECMAScript modules and imports
Convert sap.ui.define(...) to ES imports + export default. Convert sap.ui.require(...) to imports (no export). Avoid name clashes between imported modules.
Before:
sap.ui.define(["sap/ui/core/mvc/Controller"], function (Controller) {
class App extends Controller {
}
return App;
});
After:
import Controller from "sap/ui/core/mvc/Controller";
export default class App extends Controller {
}
Dynamic sap.ui.require inside method bodies → dynamic import:
import("sap/m/MessageBox").then((MessageBox) => { });
Hint: importing sap/ui/core/Core provides the singleton instance, not the class.
Step 3: Standard TypeScript Code Adaptations
- Add type information to method parameters and variables where needed.
- Add missing private member class variables (with types) to the beginning of the class definition. (In JavaScript they are often created on-the-fly during the instance lifetime.)
- Convert
someFunction.bind(...) to arrow functions (TypeScript does not propagate the bound this type into the function body).
- Define further types and structures as needed.
IMPORTANT: Never use a UI5 type with its global namespace (like sap.m.Button). Always import it from the module (like sap/m/Button) and use the imported name.
Wrong:
const b: sap.m.Button;
function getPopup(): sap.ui.core.Popup { }
Correct:
import Button from "sap/m/Button";
import Popup from "sap/ui/core/Popup";
const b: Button;
function getPopup(): Popup { }
Use UI5 control event types, not browser events like Event or MouseEvent — UI5 events are different:
import Button from "sap/m/Button";
import { Button$PressEvent } from "sap/m/Button";
import { Table$RowSelectionChangeEvent } from "sap/ui/table/Table";
export default class Main extends BaseController {
onPress(oEvent: Button$PressEvent): void {
const button = oEvent.getSource() as Button;
}
onRowSelectionChange(oEvent: Table$RowSelectionChangeEvent): void {
const selectedContext = oEvent.getParameter("rowContext");
}
}
For any event XYZ of a UI5 control ABC, types ABC$XYZEvent and ABC$XYZEventParameters are available.
Use the most specific type that provides all needed properties: KeyboardEvent/MouseEvent not Event for browser events; Button$PressEvent not sap/ui/base/Event.
Step 4: Casts for Return Values of Generic Methods
Generic methods return the super-type of all possible types although in practice it will usually be a specific sub-type. Cast the return value to the specific sub-type when needed; derive the actual type from context. This often requires an additional import. Most prominently affected: core.byId()/view.byId(), control.getBinding(), ownerComponent.getModel(), event.getSource(), component.getRootControl(), this.getOwnerComponent().
For the app controller example above, this adds an import of the app's component (AppComponent) so the cast can be done — without it, getOwnerComponent() returns a sap.ui.core.Component, which does not have the getContentDensityClass method.
import Controller from "sap/ui/core/mvc/Controller";
import AppComponent from "../Component";
export default class App extends Controller {
public onInit(): void {
this.getView().addStyleClass((this.getOwnerComponent() as AppComponent).getContentDensityClass());
}
}
Do not cast to a superclass when it's already the returned type. Avoid guessing — skip the cast if the actual type isn't clear.
Step 5: Solving any Remaining Issues
At this point remaining TypeScript errors should be vastly reduced. Fix clearly recognizable ones. In case of doubt, mention the last remaining issues to the developer.
UI5 Control TypeScript Conversion Guidelines
Converting custom UI5 controls requires specific patterns beyond the general conversion. This applies to single custom controls within applications and to control libraries.
The Runtime-Generated Methods Problem (CRITICAL)
This is the most important aspect to understand.
UI5 generates getter/setter (and more) methods for properties, aggregations, associations, and events at runtime. TypeScript cannot see them at development time. A control with a text property in its metadata will have getText()/setText() at runtime, but TypeScript errors on control.getText(). TypeScript also does not know the constructor's settings-object structure. This affects property getters/setters (getText, setText, bindText), aggregation methods (addItem, removeItem, getItems), association methods (getLabel, setLabel), event methods (attachPress, detachPress, firePress), and the constructor settings object.
The Solution: @ui5/ts-interface-generator
npm install --save-dev @ui5/ts-interface-generator@{{ts-interface-generator-version}}
Add a script to package.json to make subsequent development easier:
{
"scripts": {
"watch:controls": "npx @ui5/ts-interface-generator --watch"
}
}
NOTE: if the tsconfig covering the controls is in a subdirectory or has a different name, use --config path/to/tsconfig.json.
After converting all controls, run the generator once:
npm run watch:controls
It generates *.gen.d.ts files with interfaces for all runtime-generated methods, which TypeScript merges with the control class. Commit these files; never edit them manually.
Required Constructor Signatures (CRITICAL MANUAL STEP)
Copy the constructor signatures from the generator's terminal output into the beginning of the class body, before the metadata definition:
export default class MyControl extends Control {
constructor(id?: string | $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings) { super(id, settings); }
static readonly metadata: MetadataOptions = {
};
}
Control Metadata Typing
The control metadata must be typed as MetadataOptions:
import type { MetadataOptions } from "sap/ui/core/Element";
export default class MyControl extends Control {
static readonly metadata: MetadataOptions = {
properties: {
"text": "string"
}
};
}
- Import from
sap/ui/core/Element (or the closest base class: ManagedObject, Component); use import type (design-time only).
- Available since UI5 1.110; use
object for earlier versions.
- Typing prevents issues when inheriting from the control (inherited properties should not be repeated).
Namespace Annotation Required
The @namespace JSDoc annotation is required for the transformer to generate correct UI5 class names:
export default class MyControl extends Control {
}
Export Pattern
Must use export default immediately — a separate export breaks ts-interface-generator:
export default class MyControl extends Control {
}
class MyControl extends Control {
}
export default MyControl;
Static Members for Metadata and Renderer
Both metadata and renderer are static class members. The renderer can be inline or in a separate file:
import Control from "sap/ui/core/Control";
import type { MetadataOptions } from "sap/ui/core/Element";
import RenderManager from "sap/ui/core/RenderManager";
export default class MyControl extends Control {
static readonly metadata: MetadataOptions = {
properties: {
"text": "string"
},
events: {
"press": {}
}
};
static renderer = {
apiVersion: 2,
render: function (rm: RenderManager, control: MyControl): void {
rm.openStart("div", control);
rm.openEnd();
rm.text(control.getText());
rm.close("div");
}
};
onclick(): void {
.();
}
}
When the renderer is in a separate file (common in libraries), it should stay separate — import it (import MyControlRenderer from "./MyControlRenderer";) and assign static renderer = MyControlRenderer;.
Library-Specific Guidelines
When converting entire control libraries (not just single controls in apps), additional steps are required.
Library Module with Enums (CRITICAL to avoid XSS issues!)
In library.ts, enums must be attached to the global library object for UI5 runtime compatibility:
import ObjectPath from "sap/base/util/ObjectPath";
export enum ExampleColor {
Red = "Red",
Green = "Green",
Blue = "Blue"
}
const thisLib = ObjectPath.get("com.myorg.myui5lib") as {[key: string]: unknown};
thisLib.ExampleColor = ExampleColor;
Why this is critical for every enum in the library:
- Control properties reference types as global names:
type: "com.myorg.myui5lib.ExampleColor".
- The UI5 runtime needs to find the enum via this global path to validate the property type.
- Without the attachment, UI5 cannot validate the type → unchecked content can be written to HTML → XSS vulnerability.
Path Mapping in tsconfig.json
For libraries, add path mappings for the library namespace:
{
"compilerOptions": {
"paths": {
"com/myorg/mylib/*": ["./src/*"]
}
}
}
Control Conversion Checklist
Convert to ES6 class/module with @namespace and immediate export default; type metadata as MetadataOptions; define metadata and renderer as static members; install and run @ui5/ts-interface-generator and copy the constructor signatures from its output; attach enums to the global library object if in a library; preserve all JSDoc.
Test Conversion
There are critical, non-obvious patterns for converting UI5 test code from JavaScript to TypeScript. See the test conversion document for details when tests need to be converted.