with one click
ui5-typescript-conversion
// A skill for converting UI5 (SAPUI5/OpenUI5) projects to TypeScript.
// A skill for converting UI5 (SAPUI5/OpenUI5) projects to TypeScript.
MUST be loaded before any UI Integration Cards (also called UI5 Integration Cards) task — creating, modifying, validating, previewing, or reviewing a card, its `manifest.json`, its Configuration Editor (`dt/Configuration.js`), or any analytical chart configuration. Provides the official guidelines, validation rules, supported chart types, and Configuration Editor patterns.
UI5 development best practices and coding standards derived exclusively from official SAP UI5 guidelines. Use when writing UI5 applications to ensure modern, maintainable code following SAP standards. Covers: async module loading (sap.ui.define, ES6 imports, core:require), ComponentSupport initialization, data binding with OData types, i18n management, CSP compliance (no inline scripts), TypeScript event types (UI5 >= 1.115.0), MCP tooling (get_api_reference, run_ui5_linter), CAP integration patterns, and form creation rules (never SimpleForm, always Form with ColumnLayout). Keywords: ui5 coding standards, async loading, sap.ui.define, data binding, odata types, i18n translation, CSP no inline scripts, TypeScript event handlers, Button$PressEvent, ui5 linter, API reference, ComponentSupport, form layout, ColumnLayout, CAP integration, cds watch
| name | ui5-typescript-conversion |
| description | A skill for converting UI5 (SAPUI5/OpenUI5) projects to TypeScript. |
This document outlines how a UI5 (SAPUI5/OpenUI5) project can be converted to TypeScript. It consists of the following parts:
- Important general rules
- How the setup of the project needs to be changed
- Converting the code itself
- Converting tests (reference to separate file)
You MUST preserve existing JSDoc, documentation and comments - never remove JSDoc or comments during the conversion.
Example input:
/**
* My cool controller, it does things.
*/
return Controller.extend("com.myorg.myapp.controller.BaseController", {
/**
* Convenience method for accessing the component of the controller's view.
* @returns {sap.ui.core.Component} The component of the controller's view
*/
getOwnerComponent: function () {
// comment
return Controller.prototype.getOwnerComponent.call(this);
},
...
});
Wrong output:
export default class BaseController extends Controller {
public getOwnerComponent(): UIComponent {
return super.getOwnerComponent() as UIComponent;
}
}
Correct output:
/**
* My cool controller, it does things.
* @namespace com.myorg.myapp.controller
*/
export default class BaseController extends Controller {
/**
* Convenience method for accessing the component of the controller's view.
* @returns {sap.ui.core.Component} The component of the controller's view
*/
public getOwnerComponent(): UIComponent {
// comment
return super.getOwnerComponent() as UIComponent;
}
}
Carefully respect all guidelines in this document (and adapt appropriately where required). Before each conversion step, consider all relevant details from this document.
You should convert the project step by step, starting with the TypeScript project setup and then the most central files on which other files depend, so those other files can use the typed version of those central files once they are converted as well. "allowJs": true in the tsconfig.json's compilerOptions may be useful to run semi-converted projects if needed.
any typeDo not take shortcuts, but try to find the proper type or create an interface instead of any.
BAD:
(this.getOwnerComponent() as any).getContentDensityClass();
GOOD:
(this.getOwnerComponent() as AppComponent).getContentDensityClass()
unknown castsImport and use actual UI5 control types instead (either the base class sap/ui/core/Control or more specific classes if needed to access the respective property). Inspect the XMLView to find out which control type you actually get when calling this.byId(...) in a controller!
Don't forget using the specific event types like e.g. Route$PatternMatchedEvent for routing events.
BAD:
(this.byId("form") as unknown as {setVisible: (v: boolean) => void}).setVisible(false);
GOOD:
import SimpleForm from "sap/ui/layout/form/SimpleForm";
(this.byId("form") as SimpleForm).setVisible(false);
Many type definitions you create are useful in different files. Create those in a central location like a file in src/types/.
You must add the following dev dependencies in the package.json file (very important) if they are not already present:
{{dependencies}}
However, if a dependency is already present in package.json, do not increase the major version number of it. Do not remove existing dependencies, you must only add new configuration. Install the dependencies early to verify the types are found.
IMPORTANT: In addition, you MUST also add the @sapui5/types (or @openui5/types) package in a version matching the UI5 project as dev dependency. Framework type and version can be found in ui5.yaml or using the get_project_info MCP tool.
In addition, if (and ONLY if) dependencies or their versions changed, ensure (or tell the user) to execute npm install / yarn install (whatever is used in the project) to get the changed dependencies in the project.
The typescript-eslint dependency is only relevant when the project already has an eslint setup (details are below).
Also add the "ts-typecheck": "tsc --noEmit" script to package.json, so you and the developer can easily check for TypeScript errors.
Add a tsconfig.json file. Use the following sample as reference, but adapt to the needs of the current project, e.g. adapt the paths map:
{
"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"],
"paths": {
"com/myorg/myapp/*": ["./webapp/*"],
"unit/*": ["./webapp/test/unit/*"],
"integration/*": ["./webapp/test/integration/*"]
}
},
"exclude": ["./webapp/test/e2e/**/*"],
"include": ["./webapp/**/*"]
}
Update the ui5.yaml file to use the ui5-tooling-transpile-task and ui5-tooling-transpile-middleware and ensure that at least the following config is present:
builder:
customTasks:
- name: ui5-tooling-transpile-task
afterTask: replaceVersion
server:
customMiddleware:
- name: ui5-tooling-transpile-middleware
afterMiddleware: compression
- name: ui5-middleware-livereload
afterMiddleware: compression
Ensure that the generated ui5.yaml file is valid - avoid duplicate entries, each root configuration must only exist once.
If a configuration like server already exists, you must add to it instead of adding a second entry.
Only when the project has eslint set up, enhance the eslint configuration with TypeScript-specific parts. If eslint is not set up with dependency in package.json and an eslint config, then do nothing.
A complete eslint v9 compatible eslint.config.mjs file could e.g. look like this, but the actual content depends on the specific project, so you MUST adapt it!
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"]
}
);
Every UI5 class definitions (SuperClass.extend(...)) must be converted to a standard JavaScript class.
The properties in the UI5 class configuration object (second parameter of extend) become members of the standard JavaScript class.
It is important to annotate the class with the namespace in a JSDoc comment, so the back transformation can re-add it. This @namespace comment MUST immediately precede the class declaration.
The namespace is the part of the full package+class name (first parameter of extend) that precedes the class name.
Before (example):
[... other code, e.g. loading the dependencies "App", "Controller" etc. ...]
var App = Controller.extend("ui5tssampleapp.controller.App", {
onInit: function _onInit() {
// apply content density mode to root view
this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
}
});
After (example, do not use this code verbatim):
[... other code, e.g. loading the dependencies "App", "Controller" etc. ...]
/**
* @namespace ui5tssampleapp.controller
*/
class App extends Controller {
public onInit() {
// apply content density mode to root view
this.getView().addStyleClass((this.getOwnerComponent()).getContentDensityClass());
};
};
TypeScript UI5 apps must use modern ES modules and imports.
Hence, convert all UI5 module definition and dependency loading calls (sap.ui.require(...), sap.ui.define(...))
to ES modules with imports (and in case of sap.ui.define a module export).
In the above example, this looks as follows.
Before:
sap.ui.define(["sap/ui/core/mvc/Controller"], function (Controller) {
/**
* @namespace ui5tssampleapp.controller
*/
class App extends Controller {
... // as above
};
return App;
});
After:
import Controller from "sap/ui/core/mvc/Controller";
/**
* @namespace ui5tssampleapp.controller
*/
export default class App extends Controller {
... // as above
};
sap.ui.require shall be converted to just the imports and no export.
Avoid name clashes for the imported modules.
Hint: importing
sap/ui/core/Coredoes not provide the class (like for most other UI5 modules), but the singleton instance of the UI5 Core. So the imported module can be used directly for methods likebyId(...)instead of calls tosap.ui.getCore()which return the singleton in JavaScript.
When sap.ui.require is used dynamically, e.g. sap.ui.require(["sap/m/MessageBox"], function(MessageBox) { ... }) inside a method body, then convert this to a dynamic import like import("sap/m/MessageBox").then((MessageBox) => { ... }).
Apply your general knowledge about converting JavaScript code to TypeScript. In particular:
functions to arrow functions when someFunction.bind(...) is used because TypeScript does not seem to propagate the type of the bound "this" context into the function body.IMPORTANT: whenever you use a UI5 type, e.g. for annotating a variable or method parameter/returntype, do NOT use the UI5 type with its global namespace (like
sap.m.Buttonorsap.ui.core.Popup)! Instead, import this UI5 type from the respective module (likesap/m/Buttonorsap/ui/core/Popup- add an import if needed) and use the imported module.
Example:
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 { ... }
Hint: use the actual UI5 control events, not browser events like Event or MouseEvent, in event handlers of UI5 controls. UI5 events are different. E.g. use the Button$PressEvent and Button$PressEventParameters from the sap/m/Button module when the press event of the sap/m/Button is handled.
Note: for any event XYZ of a UI5 control ABC, types like
ABC$XYZEventandABC$XYZEventParametersare available!
Example:
Before:
sap.ui.define(["./BaseController"], function (BaseController) {
return BaseController.extend("my.app.controller.Main", {
onPress: function(oEvent) {
const button = oEvent.getSource();
},
onSelectionChange: function(oEvent) {
const items = oEvent.getParameter("selectedItems");
}
});
});
After:
import BaseController from "./BaseController";
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");
}
}
Hint: use the most specific type which does provide all needed properties. Examples:
KeyboardEvent or MouseEvent, not just Event for browser events.Button$PressEvent from the sap/m/Button module, not the sap/ui/base/Event.Generic getter methods like document.getElementById(...) or someUI5Control.getModel() or inside a controller this.byId() return the super-type of all possible types (in the examples HTMLElement and sap.ui.model.Model and sap.ui.core.Element) although in practice it will usually be a specific sub-type (e.g. an HTMLAnchorElement or a sap.ui.model.odata.v4.ODataModel or a sap.m.Input).
In many cases you will have to cast the return value to the specific type to use it. The actual type can usually be derived from the context. If not, rather avoid the cast than guessing a wrong one. Also, do not cast to a superclass like sap.ui.model.Model when this is anyway the returned type.
The same is valid for several UI5 methods, most prominently the following:
This cast will sometimes also require an additional module import to make the type (like ODataModel above) known.
In the app controller example above, this step would add an additional import of the app's component (called AppComponent), so within the onInit implementation the required typecast can be done. Without this typecast, the return type of getOwnerComponent would be a sap.ui.core.Component, which does not have the getContentDensityClass method defined in the app component.
Before:
import Controller from "sap/ui/core/mvc/Controller";
/**
* @namespace ui5tssampleapp.controller
*/
export default class App extends Controller {
public onInit() {
// apply content density mode to root view
this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
};
};
After:
import Controller from "sap/ui/core/mvc/Controller";
import AppComponent from "../Component";
/**
* @namespace ui5tssampleapp.controller
*/
export default class App extends Controller {
public onInit() : void {
// apply content density mode to root view
this.getView().addStyleClass((this.getOwnerComponent() as AppComponent).getContentDensityClass());
};
};
(Note: the "void" definition of the method return type is not strictly demanded by TypeScript, but is beneficial e.g. depending on the linting settings.)
At this point, the number of remaining TypeScript errors should be vastly reduced. If you clearly recognize some, fix them, but in case of doubt mention the last remaining issues to the developer.
This section covers the conversion of UI5 custom controls from JavaScript to TypeScript. This applies both to single custom controls within applications and to control libraries.
Converting custom UI5 controls to TypeScript requires specific patterns in addition to the general TypeScript conversion (converting the proprietary UI5 class and syntax).
This is the most important aspect to understand.
UI5 generates getter/setter (and more) methods for all properties, aggregations, associations, and events at runtime. This means TypeScript cannot see these methods at development time, causing type errors.
In a control with a text property defined in metadata:
static readonly metadata: MetadataOptions = {
properties: {
"text": "string"
}
};
TypeScript will show errors when trying to use the generated methods:
rm.text(control.getText()); // ERROR: Property 'getText' does not exist on type 'MyControl'
Additionally, TypeScript doesn't know the constructor signature structure for initializing controls:
new MyControl("myId", {text: "Hello"}); // TypeScript doesn't know about the settings object structure
This affects:
getText(), setText(), bindText()addItem(), removeItem(), getItems(), ...getLabel(), setLabel()attachPress(), detachPress(), firePress()Install the interface generator tool as a dev dependency:
npm install --save-dev @ui5/ts-interface-generator@{{ts-interface-generator-version}}
To make subsequent development easier, add a script like this to package.json:
{
"scripts": {
"watch:controls": "npx @ui5/ts-interface-generator --watch"
}
}
NOTE: the tsconfig file related to the controls must be in the same directory in which the interface generator is launched. If you launch it in the root of your project and the tsconfig covering the TypeScript controls is in a subdirectory or has a different name than tsconfig.json, then call it like npx @ui5/ts-interface-generator --watch --config path/to/tsconfig.json.
After TypeScript conversion of all controls, run the generator once to generate the needed control interfaces:
npm run watch:controls
This generates a *.gen.d.ts file (e.g., MyControl.gen.d.ts) containing TypeScript interfaces with all the runtime-generated methods. TypeScript merges these interfaces with the control class.
These generated files should be committed to version control and never edited manually.
After running the interface generator, you must manually copy the constructor signatures from the terminal output into the respective control class.
The generator outputs something like:
===== BEGIN =====
// The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures
constructor(id?: string | $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings) { super(id, settings); }
===== END =====
Copy these lines into the beginning of the class body, before the metadata definition:
export default class MyControl extends Control {
// The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures
constructor(id?: string | $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings) { super(id, settings); }
static readonly metadata: MetadataOptions = {
// ...
};
}
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"
}
};
}
Important points:
MetadataOptions from sap/ui/core/Element for controls (or closest base class - also available for sap/ui/core/Object, sap/ui/core/ManagedObject, and sap/ui/core/Component)import type instead of import (design-time only, no runtime impact)MetadataOptions available since UI5 1.110; use object for earlier versionsThe @namespace JSDoc annotation is required for the transformer to generate correct UI5 class names:
/**
* @namespace ui5.typescript.helloworld.control
*/
export default class MyControl extends Control {
// ...
}
Must use export default immediately when defining the class, otherwise ts-interface-generator will fail:
// CORRECT:
export default class MyControl extends Control {
// ...
}
// WRONG - separate export:
class MyControl extends Control {
// ...
}
export default MyControl;
Both metadata and renderer are defined as static class members:
import RenderManager from "sap/ui/core/RenderManager";
export default class MyControl extends Control {
static readonly metadata: MetadataOptions = {
properties: {
"text": "string"
}
};
static renderer = {
apiVersion: 2,
render: function (rm: RenderManager, control: MyControl): void {
rm.openStart("div", control);
rm.openEnd();
rm.text(control.getText());
rm.close("div");
}
};
}
The renderer can also be in a separate file (common in libraries) and should in this case stay separate when converting to TypeScript.
The following JavaScript code:
sap.ui.define([
"sap/ui/core/Control",
"./MyControlRenderer"
], function (Control, MyControlRenderer) {
"use strict";
return Control.extend("com.myorg.myapp.control.MyControl", {
...
renderer: MyControlRenderer,
...
is then converted to this TypeScript code:
import Control from "sap/ui/core/Control";
import type { MetadataOptions } from "sap/ui/core/Element";
import MyControlRenderer from "./MyControlRenderer";
/**
* @namespace com.myorg.myapp.control
*/
export default class MyControl extends Control {
...
static renderer = MyControlRenderer;
...
sap.ui.define([
"sap/ui/core/Control",
"sap/ui/core/RenderManager"
], function (Control, RenderManager) {
"use strict";
var MyControl = Control.extend("ui5.typescript.helloworld.control.MyControl", {
metadata: {
properties: {
"text": "string"
},
events: {
"press": {}
}
},
renderer: function (rm, control) {
rm.openStart("div", control);
rm.openEnd();
rm.text(control.getText());
rm.close("div");
},
onclick: function() {
this.firePress();
}
});
return MyControl;
});
import Control from "sap/ui/core/Control";
import type { MetadataOptions } from "sap/ui/core/Element";
import RenderManager from "sap/ui/core/RenderManager";
/**
* @namespace ui5.typescript.helloworld.control
*/
export default class MyControl extends Control {
// The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures
constructor(id?: string | $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings) { super(id, settings); }
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 {
this.firePress();
}
}
When converting entire control libraries (not just single controls in apps), additional steps are required:
In library.ts, enums must be attached to the global library object for UI5 runtime compatibility:
import ObjectPath from "sap/base/util/ObjectPath";
// Define enum as TypeScript enum
export enum ExampleColor {
Red = "Red",
Green = "Green",
Blue = "Blue"
}
// CRITICAL: Attach to global library object
const thisLib = ObjectPath.get("com.myorg.myui5lib") as {[key: string]: unknown};
thisLib.ExampleColor = ExampleColor;
Why this is critical for every enum in the library:
type: "com.myorg.myui5lib.ExampleColor"For libraries, add path mappings for the library namespace:
{
"compilerOptions": {
"paths": {
"com/myorg/mylib/*": ["./src/*"]
}
}
}
When converting a control from JavaScript to TypeScript:
@namespace JSDoc annotationexport default immediately with class definitionMetadataOptions (import from appropriate base class)static members@ui5/ts-interface-generatorThere 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..