ui5-typescript-conversion
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseUI5 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:
- Important general rules
- How the setup of the project needs to be changed
- Converting the code itself
- Converting tests (reference to separate file)
本文档介绍了如何将UI5(SAPUI5/OpenUI5)项目转换为TypeScript,包含以下几个部分:
- 重要通用规则
- 项目配置的修改方式
- 代码本身的转换步骤
- 测试代码转换(参考独立文档)
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. in the 's may be useful to run semi-converted projects if needed.
"allowJs": truetsconfig.jsoncompilerOptions您应该逐步转换项目,先从TypeScript项目配置开始,然后转换其他文件依赖的核心文件,这样其他文件转换完成后就能使用这些核心文件的类型定义。如果需要运行半转换状态的项目,的中设置会很有用。
tsconfig.jsoncompilerOptions"allowJs": trueAvoid any
type
any避免使用any
类型
anyDo not take shortcuts, but try to find the proper type or create an interface instead of .
anyBAD:
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避免unknown
类型转换
unknownImport and use actual UI5 control types instead (either the base class 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 in a controller!
Don't forget using the specific event types like e.g. for routing events.
sap/ui/core/Controlthis.byId(...)Route$PatternMatchedEvent请导入并使用实际的UI5控件类型(可以是基类,如果需要访问特定属性,也可以使用更具体的类)。在控制器中调用时,请查看XMLView以确定实际获取的控件类型!
不要忘记使用特定的事件类型,例如路由事件使用。
sap/ui/core/Controlthis.byId(...)Route$PatternMatchedEventCasting 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 (or ) package in a version matching the UI5 project as dev dependency. Framework type and version can be found in ui5.yaml or using the MCP tool.
@sapui5/types@openui5/typesget_project_infoIn 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 dependency is only relevant when the project already has an eslint setup (details are below).
typescript-eslintAlso add the script to , so you and the developer can easily check for TypeScript errors.
"ts-typecheck": "tsc --noEmit"package.json如果package.json中尚未包含以下开发依赖,您必须添加它们(非常重要):
{{dependencies}}
但如果某个依赖已存在于package.json中,请勿提升其主版本号。不要删除现有依赖,您只能添加新配置。请尽早安装依赖以验证类型是否能被正确识别。
重要提示:此外,您还必须添加与UI5项目版本匹配的(或)包作为开发依赖。框架类型和版本可在ui5.yaml中找到,或使用 MCP工具查询。
@sapui5/types@openui5/typesget_project_info另外,如果依赖或其版本发生了变化,请确保(或告知用户)执行npm install / yarn install(根据项目使用的包管理器而定)以更新项目中的依赖。
typescript-eslint同时,在中添加脚本,这样您和开发人员可以轻松检查TypeScript错误。
package.json"ts-typecheck": "tsc --noEmit"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 and and ensure that at least the following config is present:
ui5-tooling-transpile-taskui5-tooling-transpile-middlewareyaml
builder:
customTasks:
- name: ui5-tooling-transpile-task
afterTask: replaceVersion
server:
customMiddleware:
- name: ui5-tooling-transpile-middleware
afterMiddleware: compression
- name: ui5-middleware-livereload
afterMiddleware: compressionEnsure that the generated ui5.yaml file is valid - avoid duplicate entries, each root configuration must only exist once.
If a configuration like already exists, you must add to it instead of adding a second entry.
server更新ui5.yaml文件,使用和,并确保至少包含以下配置:
ui5-tooling-transpile-taskui5-tooling-transpile-middlewareyaml
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文件有效——避免重复条目,每个根配置只能存在一次。如果等配置已存在,您必须在现有配置中添加内容,而非添加第二个条目。
server4. 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 file could e.g. look like this, but the actual content depends on the specific project, so you MUST adapt it!
eslint.config.mjsjs
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.mjsjs
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 () must be converted to a standard JavaScript .
The properties in the UI5 class configuration object (second parameter of ) 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 ) that precedes the class name.
SuperClass.extend(...)classextendextendBefore (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类定义()必须转换为标准JavaScript 。
UI5类配置对象(的第二个参数)中的属性将成为标准JavaScript类的成员。
必须在类上添加包含命名空间的JSDoc注释,以便后续反向转换时可以重新添加命名空间。这个注释必须紧跟在类声明之前。
命名空间是完整包+类名(的第一个参数)中类名之前的部分。
SuperClass.extend(...)classextend@namespaceextend转换前(示例):
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 (, )
to ES modules with imports (and in case of a module export).
sap.ui.require(...)sap.ui.define(...)sap.ui.defineIn 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.requireHint: importingdoes 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 likesap/ui/core/Coreinstead of calls tobyId(...)which return the singleton in JavaScript.sap.ui.getCore()
When is used dynamically, e.g. inside a method body, then convert this to a dynamic import like .
sap.ui.requiresap.ui.require(["sap/m/MessageBox"], function(MessageBox) { ... })import("sap/m/MessageBox").then((MessageBox) => { ... })TypeScript UI5应用必须使用现代ES模块和导入语法。
因此,需将所有UI5模块定义和依赖加载调用(、)转换为带导入的ES模块(如果是则需添加模块导出)。
sap.ui.require(...)sap.ui.define(...)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提示:导入不会提供类(与大多数其他UI5模块不同),而是提供UI5 Core的单例实例。因此,导入的模块可直接用于sap/ui/core/Core等方法,而非JavaScript中返回单例的byId(...)调用。sap.ui.getCore()
当被动态使用时,例如在方法体内使用,则需转换为动态导入,如。
sap.ui.requiresap.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 s to arrow functions when
functionis used because TypeScript does not seem to propagate the type of the bound "this" context into the function body.someFunction.bind(...) - 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 (likeorsap.m.Button)! Instead, import this UI5 type from the respective module (likesap.ui.core.Popuporsap/m/Button- add an import if needed) and use the imported module.sap/ui/core/Popup
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 or , in event handlers of UI5 controls. UI5 events are different. E.g. use the and from the module when the event of the is handled.
EventMouseEventButton$PressEventButton$PressEventParameterssap/m/Buttonpresssap/m/ButtonNote: for any event XYZ of a UI5 control ABC, types likeandABC$XYZEventare available!ABC$XYZEventParameters
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 or
KeyboardEvent, not justMouseEventfor browser events.Event - Use the from the
Button$PressEventmodule, not thesap/m/Button.sap/ui/base/Event - The same is valid for all types, not only events.
运用您将JavaScript代码转换为TypeScript的通用知识。尤其注意:
- 必要时为方法参数和变量添加类型信息。
- 在类定义开头添加缺失的私有成员变量(带类型信息)。(在JavaScript中,这些变量通常在类实例生命周期中动态创建。)
- 当使用时,将常规
someFunction.bind(...)转换为箭头函数,因为TypeScript似乎不会将绑定的function上下文类型传播到函数体内。this - 如有需要,定义代码中所需的更多类型和结构。
重要提示:无论何时使用UI5类型(例如为变量或方法参数/返回类型添加注释),请勿使用带全局命名空间的UI5类型(如或sap.m.Button)!相反,请从相应模块(如sap.ui.core.Popup或sap/m/Button——必要时添加导入)导入此UI5类型,并使用导入的模块。sap/ui/core/Popup
示例:
错误示例:
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控件事件,而非浏览器事件如或。UI5事件有所不同。例如,处理的事件时,请使用模块中的和。
EventMouseEventsap/m/Buttonpresssap/m/ButtonButton$PressEventButton$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 or or inside a controller return the super-type of all possible types (in the examples and and ) although in practice it will usually be a specific sub-type (e.g. an or a or a ).
document.getElementById(...)someUI5Control.getModel()this.byId()HTMLElementsap.ui.model.Modelsap.ui.core.ElementHTMLAnchorElementsap.ui.model.odata.v4.ODataModelsap.m.InputIn 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 when this is anyway the returned type.
sap.ui.model.ModelThe 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 above) known.
ODataModelIn the app controller example above, this step would add an additional import of the app's component (called ), so within the implementation the required typecast can be done. Without this typecast, the return type of would be a , which does not have the method defined in the app component.
AppComponentonInitgetOwnerComponentsap.ui.core.ComponentgetContentDensityClassBefore:
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()HTMLElementsap.ui.model.Modelsap.ui.core.ElementHTMLAnchorElementsap.ui.model.odata.v4.ODataModelsap.m.Input在许多情况下,您需要将返回值转换为特定类型才能使用。实际类型通常可从上下文推断。如果无法推断,宁可避免转换也不要猜测错误类型。另外,当返回类型本身就是超类(如)时,无需转换为该超类。
sap.ui.model.Model此规则同样适用于多个UI5方法,最常见的包括:
- core.byId() / view.byId()
- control.getBinding()
- ownerComponent.getModel()
- event.getSource()
- component.getRootControl()
- this.getOwnerComponent()
这种转换有时还需要额外的模块导入,以使类型(如上述的)被识别。
ODataModel在上述应用控制器示例中,此步骤需要额外导入应用的组件(名为),以便在实现中进行必要的类型转换。如果没有此类型转换,的返回类型将是,而该类型没有应用组件中定义的方法。
AppComponentonInitgetOwnerComponentsap.ui.core.ComponentgetContentDensityClass转换前:
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());
};
};(注意:方法返回类型的定义并非TypeScript强制要求,但根据代码检查设置,这样做会更有益。)
voidStep 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 property defined in metadata:
texttypescript
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 structureThis 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
在元数据中定义了属性的控件中:
texttypescript
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.jsonjson
{
"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 , then call it like .
tsconfig.json npx @ui5/ts-interface-generator --watch --config path/to/tsconfig.jsonAfter TypeScript conversion of all controls, run the generator once to generate the needed control interfaces:
bash
npm run watch:controlsThis generates a file (e.g., ) containing TypeScript interfaces with all the runtime-generated methods. TypeScript merges these interfaces with the control class.
*.gen.d.tsMyControl.gen.d.tsThese 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.jsonjson
{
"scripts": {
"watch:controls": "npx @ui5/ts-interface-generator --watch"
}
}注意:与控件相关的tsconfig文件必须与接口生成工具启动的目录相同。如果您在项目根目录启动工具,而覆盖TypeScript控件的tsconfig在子目录中或文件名不是,则需使用如下命令调用:。
tsconfig.jsonnpx @ui5/ts-interface-generator --watch --config path/to/tsconfig.json完成所有控件的TypeScript转换后,运行一次生成工具以生成所需的控件接口:
bash
npm run watch:controls这会生成一个文件(例如),其中包含带有所有运行时生成方法的TypeScript接口。TypeScript会将这些接口与控件类合并。
*.gen.d.tsMyControl.gen.d.ts这些生成的文件应提交到版本控制中,且绝不能手动编辑。
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 :
MetadataOptionstypescript
import type { MetadataOptions } from "sap/ui/core/Element";
export default class MyControl extends Control {
static readonly metadata: MetadataOptions = {
properties: {
"text": "string"
}
};
}Important points:
- Import from
MetadataOptionsfor controls (or closest base class - also available forsap/ui/core/Element,sap/ui/core/Object, andsap/ui/core/ManagedObject)sap/ui/core/Component - Use instead of
import type(design-time only, no runtime impact)import - available since UI5 1.110; use
MetadataOptionsfor earlier versionsobject - Typing prevents issues when inheriting from the control (inherited properties should not be repeated)
控件元数据必须标注为类型:
MetadataOptionstypescript
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 - 从UI5 1.110版本开始可用;对于更早版本,使用
MetadataOptionsobject - 类型标注可避免继承控件时出现问题(不应重复继承的属性)
Namespace Annotation Required
必需的命名空间注释
The JSDoc annotation is required for the transformer to generate correct UI5 class names:
@namespacetypescript
/**
* @namespace ui5.typescript.helloworld.control
*/
export default class MyControl extends Control {
// ...
}@namespacetypescript
/**
* @namespace ui5.typescript.helloworld.control
*/
export default class MyControl extends Control {
// ...
}Export Pattern
导出模式
Must use immediately when defining the class, otherwise ts-interface-generator will fail:
export defaulttypescript
// CORRECT:
export default class MyControl extends Control {
// ...
}
// WRONG - separate export:
class MyControl extends Control {
// ...
}
export default MyControl;必须在定义类时立即使用,否则ts-interface-generator会运行失败:
export defaulttypescript
// 正确:
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 class members:
statictypescript
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;
...元数据和渲染器都定义为类成员:
statictypescript
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 , enums must be attached to the global library object for UI5 runtime compatibility:
library.tstypescript
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
在中,枚举必须附加到全局库对象以兼容UI5运行时:
library.tstypescript
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:
- Convert to ES6 class/module like regular UI5 modules
- Add JSDoc annotation
@namespace - Use immediately with class definition
export default - Type metadata as (import from appropriate base class)
MetadataOptions - Define metadata and renderer as members
static - Install and run
@ui5/ts-interface-generator - Copy constructor signatures from generator output into class
- If in a library: manually attach enums to global library object
- Preserve all JSDoc comments and documentation
将控件从JavaScript转换为TypeScript时:
- 像常规UI5模块一样转换为ES6类/模块
- 添加JSDoc注释
@namespace - 定义类时立即使用
export default - 将元数据标注为类型(从适当的基类导入)
MetadataOptions - 将元数据和渲染器定义为成员
static - 安装并运行
@ui5/ts-interface-generator - 将生成器输出中的构造函数签名复制到类中
- 如果是库中的控件:手动将枚举附加到全局库对象
- 保留所有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有一些关键且不明显的模式。如需转换测试代码,请参阅测试转换文档了解详细信息。