ui5-typescript-conversion

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

UI5 TypeScript Conversion Guidelines

UI5 转 TypeScript 转换指南

This document outlines how a UI5 (SAPUI5/OpenUI5) project can be converted to TypeScript. It consists of the following parts:
  1. Important general rules
  2. How the setup of the project needs to be changed
  3. Converting the code itself
  4. Converting tests (reference to separate file)
本文档介绍了如何将UI5(SAPUI5/OpenUI5)项目转换为TypeScript,包含以下几个部分:
  1. 重要通用规则
  2. 项目配置的修改方式
  3. 代码本身的转换步骤
  4. 测试代码转换(参考独立文档)

General Conversion Rules

通用转换规则

Preserve ALL comments

保留所有注释

You MUST preserve existing JSDoc, documentation and comments - never remove JSDoc or comments during the conversion.
Example input:
js
/**
 * 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:
ts
export default class BaseController extends Controller {
    public getOwnerComponent(): UIComponent {
        return super.getOwnerComponent() as UIComponent;
    }
}
Correct output:
ts
/**
 * 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;
    }
}
您必须保留现有的JSDoc、文档和注释——转换过程中绝不能删除JSDoc或注释。
示例输入:
js
/**
 * 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);
    },
    ...
});
错误输出:
ts
export default class BaseController extends Controller {
    public getOwnerComponent(): UIComponent {
        return super.getOwnerComponent() as UIComponent;
    }
}
正确输出:
ts
/**
 * 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;
    }
}

Be diligent

严谨细致

Carefully respect all guidelines in this document (and adapt appropriately where required). Before each conversion step, consider all relevant details from this document.
请严格遵守本文档中的所有指南(必要时适当调整)。在执行每个转换步骤前,请考虑本文档中的所有相关细节。

Go step-by-step

分步转换

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.
您应该逐步转换项目,先从TypeScript项目配置开始,然后转换其他文件依赖的核心文件,这样其他文件转换完成后就能使用这些核心文件的类型定义。如果需要运行半转换状态的项目,
tsconfig.json
的
compilerOptions
中设置
"allowJs": true
会很有用。

Avoid
any
type

避免使用
any
类型

Do not take shortcuts, but try to find the proper type or create an interface instead of
any
.
BAD:
ts
(this.getOwnerComponent() as any).getContentDensityClass();
GOOD:
ts
(this.getOwnerComponent() as AppComponent).getContentDensityClass()
不要走捷径,应尝试找到合适的类型或创建接口,而非使用
any
。
错误示例:
ts
(this.getOwnerComponent() as any).getContentDensityClass();
正确示例:
ts
(this.getOwnerComponent() as AppComponent).getContentDensityClass()

Avoid
unknown
casts

避免
unknown
类型转换

Import 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.
请导入并使用实际的UI5控件类型(可以是基类
sap/ui/core/Control
,如果需要访问特定属性,也可以使用更具体的类)。在控制器中调用
this.byId(...)
时,请查看XMLView以确定实际获取的控件类型! 不要忘记使用特定的事件类型,例如路由事件使用
Route$PatternMatchedEvent
。

Casting Example

类型转换示例

BAD:
ts
(this.byId("form") as unknown as {setVisible: (v: boolean) => void}).setVisible(false);
GOOD:
ts
import SimpleForm from "sap/ui/layout/form/SimpleForm";
(this.byId("form") as SimpleForm).setVisible(false);
错误示例:
ts
(this.byId("form") as unknown as {setVisible: (v: boolean) => void}).setVisible(false);
正确示例:
ts
import SimpleForm from "sap/ui/layout/form/SimpleForm";
(this.byId("form") as SimpleForm).setVisible(false);

Create shared type definitions

创建共享类型定义

Many type definitions you create are useful in different files. Create those in a central location like a file in
src/types/
.
您创建的许多类型定义在不同文件中都有用。请将这些定义放在中心位置,例如
src/types/
目录下的文件中。

Project Setup Conversion

项目配置转换

1. package.json

1. package.json

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.
如果package.json中尚未包含以下开发依赖,您必须添加它们(非常重要):
{{dependencies}}
但如果某个依赖已存在于package.json中,请勿提升其主版本号。不要删除现有依赖,您只能添加新配置。请尽早安装依赖以验证类型是否能被正确识别。
重要提示:此外,您还必须添加与UI5项目版本匹配的
@sapui5/types
(或
@openui5/types
)包作为开发依赖。框架类型和版本可在ui5.yaml中找到,或使用
get_project_info
MCP工具查询。
另外,如果依赖或其版本发生了变化,请确保(或告知用户)执行npm install / yarn install(根据项目使用的包管理器而定)以更新项目中的依赖。
typescript-eslint
依赖仅在项目已配置eslint时才相关(详情如下)。
同时,在
package.json
中添加
"ts-typecheck": "tsc --noEmit"
脚本,这样您和开发人员可以轻松检查TypeScript错误。

2. tsconfig.json

2. tsconfig.json

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:
json
{
	"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/**/*"]
}
添加tsconfig.json文件。以下面的示例为参考,但需根据当前项目需求调整,例如调整路径映射:
json
{
	"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/**/*"]
}

3. ui5.yaml

3. ui5.yaml

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:
yaml
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.
更新ui5.yaml文件,使用
ui5-tooling-transpile-task
和
ui5-tooling-transpile-middleware
,并确保至少包含以下配置:
yaml
builder:
  customTasks:
    - name: ui5-tooling-transpile-task
      afterTask: replaceVersion
server:
  customMiddleware:
    - name: ui5-tooling-transpile-middleware
      afterMiddleware: compression
    - name: ui5-middleware-livereload
      afterMiddleware: compression
确保生成的ui5.yaml文件有效——避免重复条目,每个根配置只能存在一次。如果
server
等配置已存在,您必须在现有配置中添加内容,而非添加第二个条目。

4. Eslint configuration

4. Eslint配置

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!
js
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"]
	}
);
仅当项目已配置eslint时,才用TypeScript相关部分增强eslint配置。如果package.json中没有eslint依赖,也没有eslint配置文件,则无需操作。 一个完整的兼容eslint v9的
eslint.config.mjs
文件示例如下,但实际内容需根据具体项目调整,您必须进行适配!
js
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 proprietary UI5 class syntax to standard ES class syntax

步骤1:将专有UI5类语法转换为标准ES类语法

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):
js
[... 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):
js
[... 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());
    };
};
所有UI5类定义(
SuperClass.extend(...)
)必须转换为标准JavaScript
class
。 UI5类配置对象(
extend
的第二个参数)中的属性将成为标准JavaScript类的成员。 必须在类上添加包含命名空间的JSDoc注释,以便后续反向转换时可以重新添加命名空间。这个
@namespace
注释必须紧跟在类声明之前。 命名空间是完整包+类名(
extend
的第一个参数)中类名之前的部分。
转换前(示例):
js
[... 其他代码,例如加载依赖"App"、"Controller"等 ...]
 
var App = Controller.extend("ui5tssampleapp.controller.App", {
    onInit: function _onInit() {
        // apply content density mode to root view
        this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
    }
});
转换后(示例,请勿直接使用此代码):
js
[... 其他代码,例如加载依赖"App"、"Controller"等 ...]
 
/**
* @namespace ui5tssampleapp.controller
*/
class App extends Controller {
    public onInit() {
        // apply content density mode to root view
        this.getView().addStyleClass((this.getOwnerComponent()).getContentDensityClass());
    };
};

Step 2: Change to ECMAScript modules and imports

步骤2:转换为ECMAScript模块和导入语法

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:
js
sap.ui.define(["sap/ui/core/mvc/Controller"], function (Controller) {
    /**
     * @namespace ui5tssampleapp.controller
     */
    class App extends Controller {
        ... // as above
    };
 
  return App;
});
After:
js
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/Core
does 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 like
byId(...)
instead of calls to
sap.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) => { ... })
.
TypeScript UI5应用必须使用现代ES模块和导入语法。 因此,需将所有UI5模块定义和依赖加载调用(
sap.ui.require(...)
、
sap.ui.define(...)
)转换为带导入的ES模块(如果是
sap.ui.define
则需添加模块导出)。
以上述示例为例,转换如下:
转换前:
js
sap.ui.define(["sap/ui/core/mvc/Controller"], function (Controller) {
    /**
     * @namespace ui5tssampleapp.controller
     */
    class App extends Controller {
        ... // 同上述代码
    };
 
  return App;
});
转换后:
js
import Controller from "sap/ui/core/mvc/Controller";
 
/**
* @namespace ui5tssampleapp.controller
*/
export default class App extends Controller {
    ... // 同上述代码
};
sap.ui.require
应转换为仅保留导入,无需导出。 避免导入模块出现名称冲突。
提示:导入
sap/ui/core/Core
不会提供类(与大多数其他UI5模块不同),而是提供UI5 Core的单例实例。因此,导入的模块可直接用于
byId(...)
等方法,而非JavaScript中返回单例的
sap.ui.getCore()
调用。
当
sap.ui.require
被动态使用时,例如在方法体内使用
sap.ui.require(["sap/m/MessageBox"], function(MessageBox) { ... })
,则需转换为动态导入,如
import("sap/m/MessageBox").then((MessageBox) => { ... })
。

Step 3: Standard TypeScript Code Adaptations

步骤3:标准TypeScript代码适配

Apply your general knowledge about converting JavaScript code to TypeScript. In particular:
  • Add type information to method parameters and variables where needed.
  • Add missing private member class variables (with type information) to the beginning of the class definition. (In JavaScript they are often created later on-the-fly during the lifetime of a class instance.)
  • Convert conventional
    function
    s 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.
  • Define further types and structures needed within the code, if applicable.
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.Button
or
sap.ui.core.Popup
)! Instead, import this UI5 type from the respective module (like
sap/m/Button
or
sap/ui/core/Popup
- add an import if needed) and use the imported module.
Example:
Wrong:
ts
const b: sap.m.Button;
function getPopup(): sap.ui.core.Popup  { ... }
Correct:
ts
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$XYZEvent
and
ABC$XYZEventParameters
are available!
Example:
Before:
js
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:
ts
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:
  • Use specific types like
    KeyboardEvent
    or
    MouseEvent
    , not just
    Event
    for browser events.
  • Use the
    Button$PressEvent
    from the
    sap/m/Button
    module, not the
    sap/ui/base/Event
    .
  • The same is valid for all types, not only events.
运用您将JavaScript代码转换为TypeScript的通用知识。尤其注意:
  • 必要时为方法参数和变量添加类型信息。
  • 在类定义开头添加缺失的私有成员变量(带类型信息)。(在JavaScript中,这些变量通常在类实例生命周期中动态创建。)
  • 当使用
    someFunction.bind(...)
    时,将常规
    function
    转换为箭头函数,因为TypeScript似乎不会将绑定的
    this
    上下文类型传播到函数体内。
  • 如有需要,定义代码中所需的更多类型和结构。
重要提示:无论何时使用UI5类型(例如为变量或方法参数/返回类型添加注释),请勿使用带全局命名空间的UI5类型(如
sap.m.Button
或
sap.ui.core.Popup
)!相反,请从相应模块(如
sap/m/Button
或
sap/ui/core/Popup
——必要时添加导入)导入此UI5类型,并使用导入的模块。
示例:
错误示例:
ts
const b: sap.m.Button;
function getPopup(): sap.ui.core.Popup  { ... }
正确示例:
ts
import Button from "sap/m/Button";
import Popup from "sap/ui/core/Popup";
 
const b: Button;
function getPopup(): Popup  { ... }
提示:在UI5控件的事件处理程序中,请使用实际的UI5控件事件,而非浏览器事件如
Event
或
MouseEvent
。UI5事件有所不同。例如,处理
sap/m/Button
的
press
事件时,请使用
sap/m/Button
模块中的
Button$PressEvent
和
Button$PressEventParameters
。
注意:对于UI5控件ABC的任何事件XYZ,都有
ABC$XYZEvent
和
ABC$XYZEventParameters
等类型可用!
示例:
转换前:
js
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");
        }
    });
});
转换后:
ts
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");
    }
}
提示:使用能提供所有所需属性的最具体类型。例如:
  • 对于浏览器事件,使用
    KeyboardEvent
    或
    MouseEvent
    等具体类型,而非仅使用
    Event
    。
  • 使用
    sap/m/Button
    模块中的
    Button$PressEvent
    ,而非
    sap/ui/base/Event
    。
  • 此规则适用于所有类型,而非仅事件。

Step 4: Casts for Return Values of Generic Methods

步骤4:通用方法返回值的类型转换

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:
  • core.byId() / view.byId()
  • control.getBinding()
  • ownerComponent.getModel()
  • event.getSource()
  • component.getRootControl()
  • this.getOwnerComponent()
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:
js
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:
ts
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.)
通用 getter 方法如
document.getElementById(...)
、
someUI5Control.getModel()
或控制器中的
this.byId()
返回所有可能类型的超类型(在示例中为
HTMLElement
、
sap.ui.model.Model
和
sap.ui.core.Element
),但实际上通常是特定的子类型(例如
HTMLAnchorElement
、
sap.ui.model.odata.v4.ODataModel
或
sap.m.Input
)。
在许多情况下,您需要将返回值转换为特定类型才能使用。实际类型通常可从上下文推断。如果无法推断,宁可避免转换也不要猜测错误类型。另外,当返回类型本身就是超类(如
sap.ui.model.Model
)时,无需转换为该超类。
此规则同样适用于多个UI5方法,最常见的包括:
  • core.byId() / view.byId()
  • control.getBinding()
  • ownerComponent.getModel()
  • event.getSource()
  • component.getRootControl()
  • this.getOwnerComponent()
这种转换有时还需要额外的模块导入,以使类型(如上述的
ODataModel
)被识别。
在上述应用控制器示例中,此步骤需要额外导入应用的组件(名为
AppComponent
),以便在
onInit
实现中进行必要的类型转换。如果没有此类型转换,
getOwnerComponent
的返回类型将是
sap.ui.core.Component
,而该类型没有应用组件中定义的
getContentDensityClass
方法。
转换前:
js
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());
    };
 
};
转换后:
ts
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());
    };
 
};
(注意:方法返回类型的
void
定义并非TypeScript强制要求,但根据代码检查设置,这样做会更有益。)

Step 5: Solving any Remaining Issues

步骤5:解决剩余问题

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.
此时,剩余的TypeScript错误数量应大幅减少。 如果您能明确识别某些错误,请修复它们;如有疑问,请向开发人员说明剩余的最后问题。

UI5 Control TypeScript Conversion Guidelines

UI5 控件 TypeScript 转换指南

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).
本节介绍如何将UI5自定义控件从JavaScript转换为TypeScript。这既适用于应用中的单个自定义控件,也适用于控件库。
将自定义UI5控件转换为TypeScript,除了通用TypeScript转换(转换专有UI5类和语法)外,还需要特定的模式。

The Runtime-Generated Methods Problem (CRITICAL)

运行时生成方法的问题(关键)

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.
这是需要理解的最重要方面。
UI5会在运行时为所有属性、聚合、关联和事件生成getter/setter(及更多)方法。这意味着TypeScript在开发时无法看到这些方法,从而导致类型错误。

The Problem

问题描述

In a control with a
text
property defined in metadata:
typescript
static readonly metadata: MetadataOptions = {
    properties: {
        "text": "string"
    }
};
TypeScript will show errors when trying to use the generated methods:
typescript
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:
typescript
new MyControl("myId", {text: "Hello"}); // TypeScript doesn't know about the 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()
  • Constructor settings object structure
在元数据中定义了
text
属性的控件中:
typescript
static readonly metadata: MetadataOptions = {
    properties: {
        "text": "string"
    }
};
当尝试使用生成的方法时,TypeScript会显示错误:
typescript
rm.text(control.getText());  // 错误:类型'MyControl'上不存在属性'getText'
此外,TypeScript不知道用于初始化控件的构造函数签名结构:
typescript
new MyControl("myId", {text: "Hello"}); // TypeScript不知道设置对象的结构
这会影响:
  • 属性getter/setter:
    getText()
    、
    setText()
    、
    bindText()
  • 聚合方法:
    addItem()
    、
    removeItem()
    、
    getItems()
    等
  • 关联方法:
    getLabel()
    、
    setLabel()
  • 事件方法:
    attachPress()
    、
    detachPress()
    、
    firePress()
  • 构造函数设置对象结构

The Solution: @ui5/ts-interface-generator

解决方案:@ui5/ts-interface-generator

Install the interface generator tool as a dev dependency:
sh
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
:
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:
bash
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.
将接口生成工具安装为开发依赖:
sh
npm install --save-dev @ui5/ts-interface-generator@{{ts-interface-generator-version}}
为了简化后续开发,请在
package.json
中添加如下脚本:
json
{
    "scripts": {
        "watch:controls": "npx @ui5/ts-interface-generator --watch"
    }
}
注意:与控件相关的tsconfig文件必须与接口生成工具启动的目录相同。如果您在项目根目录启动工具,而覆盖TypeScript控件的tsconfig在子目录中或文件名不是
tsconfig.json
,则需使用如下命令调用:
npx @ui5/ts-interface-generator --watch --config path/to/tsconfig.json
。
完成所有控件的TypeScript转换后,运行一次生成工具以生成所需的控件接口:
bash
npm run watch:controls
这会生成一个
*.gen.d.ts
文件(例如
MyControl.gen.d.ts
),其中包含带有所有运行时生成方法的TypeScript接口。TypeScript会将这些接口与控件类合并。
这些生成的文件应提交到版本控制中,且绝不能手动编辑。

Required Constructor Signatures (CRITICAL MANUAL STEP)

必需的构造函数签名(关键手动步骤)

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:
typescript
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 = {
        // ...
    };
}
运行生成工具后,您必须将终端输出中的构造函数签名手动复制到相应的控件类中。
生成工具会输出类似如下内容:
===== BEGIN =====
// 以下三行由生成工具生成,应保持原样,以使TypeScript识别构造函数签名 
constructor(id?: string | $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings) { super(id, settings); }
===== END =====
将这些行复制到类体的开头,放在元数据定义之前:
typescript
export default class MyControl extends Control {
    // 以下三行由生成工具生成,应保持原样,以使TypeScript识别构造函数签名 
    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
:
typescript
import type { MetadataOptions } from "sap/ui/core/Element";

export default class MyControl extends Control {
    static readonly metadata: MetadataOptions = {
        properties: {
            "text": "string"
        }
    };
}
Important points:
  • Import
    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
    )
  • Use
    import type
    instead of
    import
    (design-time only, no runtime impact)
  • MetadataOptions
    available since UI5 1.110; use
    object
    for earlier versions
  • Typing prevents issues when inheriting from the control (inherited properties should not be repeated)
控件元数据必须标注为
MetadataOptions
类型:
typescript
import type { MetadataOptions } from "sap/ui/core/Element";

export default class MyControl extends Control {
    static readonly metadata: MetadataOptions = {
        properties: {
            "text": "string"
        }
    };
}
重要提示:
  • 从
    sap/ui/core/Element
    导入
    MetadataOptions
    (或最接近的基类——
    sap/ui/core/Object
    、
    sap/ui/core/ManagedObject
    和
    sap/ui/core/Component
    也提供此类型)
  • 使用
    import type
    而非
    import
    (仅在设计时使用,无运行时影响)
  • MetadataOptions
    从UI5 1.110版本开始可用;对于更早版本,使用
    object
  • 类型标注可避免继承控件时出现问题(不应重复继承的属性)

Namespace Annotation Required

必需的命名空间注释

The
@namespace
JSDoc annotation is required for the transformer to generate correct UI5 class names:
typescript
/**
 * @namespace ui5.typescript.helloworld.control
 */
export default class MyControl extends Control {
    // ...
}
@namespace
JSDoc注释对于转换器生成正确的UI5类名是必需的:
typescript
/**
 * @namespace ui5.typescript.helloworld.control
 */
export default class MyControl extends Control {
    // ...
}

Export Pattern

导出模式

Must use
export default
immediately when defining the class
, otherwise ts-interface-generator will fail:
typescript
// CORRECT:
export default class MyControl extends Control {
    // ...
}

// WRONG - separate export:
class MyControl extends Control {
    // ...
}
export default MyControl;
必须在定义类时立即使用
export default
,否则ts-interface-generator会运行失败:
typescript
// 正确:
export default class MyControl extends Control {
    // ...
}

// 错误 - 单独导出:
class MyControl extends Control {
    // ...
}
export default MyControl;

Static Members for Metadata and Renderer

元数据和渲染器的静态成员

Both metadata and renderer are defined as
static
class members:
typescript
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:
javascript
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:
typescript
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;
    ...
元数据和渲染器都定义为
static
类成员:
typescript
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");
        }
    };
}
渲染器也可以放在单独的文件中(在库中很常见),转换为TypeScript时应保持单独存放。
以下JavaScript代码:
javascript
sap.ui.define([
    "sap/ui/core/Control",
    "./MyControlRenderer"
], function (Control, MyControlRenderer) {
    "use strict";

    return Control.extend("com.myorg.myapp.control.MyControl", {
        ...
        renderer: MyControlRenderer,
        ...
将转换为如下TypeScript代码:
typescript
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;
    ...

Complete Control Example

完整控件示例

JavaScript (Before):

JavaScript(转换前):

javascript
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;
});
javascript
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;
});

TypeScript (After):

TypeScript(转换后):

typescript
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();
    }
}
typescript
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 {
    // 以下三行由生成工具生成,应保持原样,以使TypeScript识别构造函数签名 
    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();
    }
}

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!)

包含枚举的库模块(关键!避免XSS问题)

In
library.ts
, enums must be attached to the global library object for UI5 runtime compatibility:
typescript
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:
  • Control properties reference types as global names:
    type: "com.myorg.myui5lib.ExampleColor"
  • UI5 runtime needs to find the enum via this global path
  • Without this, UI5 cannot validate the property type
  • This breaks type checking and can create XSS vulnerabilities as unchecked content can be written to HTML unexpectedly
在
library.ts
中,枚举必须附加到全局库对象以兼容UI5运行时:
typescript
import ObjectPath from "sap/base/util/ObjectPath";

// 将枚举定义为TypeScript枚举
export enum ExampleColor {
    Red = "Red",
    Green = "Green",
    Blue = "Blue"
}

// 关键:附加到全局库对象
const thisLib = ObjectPath.get("com.myorg.myui5lib") as {[key: string]: unknown};
thisLib.ExampleColor = ExampleColor;
为什么库中的每个枚举都需要这样做(关键):
  • 控件属性引用类型为全局名称:
    type: "com.myorg.myui5lib.ExampleColor"
  • UI5运行时需要通过此全局路径找到枚举
  • 否则,UI5无法验证属性类型
  • 这会破坏类型检查,并可能因未检查的内容意外写入HTML而产生XSS漏洞

Path Mapping in tsconfig.json

tsconfig.json中的路径映射

For libraries, add path mappings for the library namespace:
json
{
    "compilerOptions": {
        "paths": {
            "com/myorg/mylib/*": ["./src/*"]
        }
    }
}
对于库,需为库命名空间添加路径映射:
json
{
    "compilerOptions": {
        "paths": {
            "com/myorg/mylib/*": ["./src/*"]
        }
    }
}

Control Conversion Checklist

控件转换检查清单

When converting a control from JavaScript to TypeScript:
  1. Convert to ES6 class/module like regular UI5 modules
  2. Add
    @namespace
    JSDoc annotation
  3. Use
    export default
    immediately with class definition
  4. Type metadata as
    MetadataOptions
    (import from appropriate base class)
  5. Define metadata and renderer as
    static
    members
  6. Install and run
    @ui5/ts-interface-generator
  7. Copy constructor signatures from generator output into class
  8. If in a library: manually attach enums to global library object
  9. Preserve all JSDoc comments and documentation
将控件从JavaScript转换为TypeScript时:
  1. 像常规UI5模块一样转换为ES6类/模块
  2. 添加
    @namespace
    JSDoc注释
  3. 定义类时立即使用
    export default
  4. 将元数据标注为
    MetadataOptions
    类型(从适当的基类导入)
  5. 将元数据和渲染器定义为
    static
    成员
  6. 安装并运行
    @ui5/ts-interface-generator
  7. 将生成器输出中的构造函数签名复制到类中
  8. 如果是库中的控件:手动将枚举附加到全局库对象
  9. 保留所有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..
将UI5测试代码从JavaScript转换为TypeScript有一些关键且不明显的模式。如需转换测试代码,请参阅测试转换文档了解详细信息。