关于 ui5-typescript-conversion
面向 SAPUI5/OpenUI5 项目的 JavaScript 转 TypeScript 指南,按步骤涵盖项目配置、应用代码和测试。重点是保留注释,并使用具体的 UI5 类型。
主要功能
- 01项目设置与 tsconfig 指引
- 02UI5 类和模块转换
- 03方法、变量与事件类型标注
- 04共享类型定义
- 05注释保留规则
适用场景
- 01规划 UI5 项目的 TypeScript 迁移
- 02转换 UI5 类和模块定义
- 03添加精确的 UI5 控件与事件类型
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cline/ skills --skill ui5-typescript-conversion安装后在 Claude Code 中输入/ui5-typescript-conversion确认已加载。
附带文件references/test_conversion.md
详细介绍
UI5 TypeScript 转换指南
本文概述了如何将 UI5(SAPUI5/OpenUI5)项目转换为 TypeScript。内容包括以下部分:
- 重要的一般规则
- 如何更改项目设置
- 转换代码本身
- 转换测试(参见单独的文件)
一般转换规则
保留所有注释
你必须保留现有的 JSDoc、文档和注释——转换期间绝不能删除 JSDoc 或注释。
输入示例:
/**
* 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);
},
...
});
错误的输出:
export default class BaseController extends Controller {
public getOwnerComponent(): UIComponent {
return super.getOwnerComponent() as UIComponent;
}
}
正确的输出:
/**
* 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;
}
}
认真细致
仔细遵循本文档中的所有指南(并在需要时进行适当调整)。每次转换前,都要考虑本文档中的所有相关细节。
逐步进行
你应逐步转换项目,先设置 TypeScript 项目,然后转换最核心的文件;其他文件依赖这些文件,因此在这些核心文件完成转换后,其他文件便可以使用它们的类型版本。如有需要,在运行部分转换的项目时,tsconfig.json 的 compilerOptions 中的 "allowJs": true 可能会有所帮助。
避免使用 any 类型
不要走捷径,应尝试寻找合适的类型或创建接口,而不是使用 any。
不推荐:
(this.getOwnerComponent() as any).getContentDensityClass();
推荐:
(this.getOwnerComponent() as AppComponent).getContentDensityClass()
避免转换为 unknown
请导入并使用实际的 UI5 控件类型(可以是基类 sap/ui/core/Control,也可以是为了访问相应属性而使用的更具体的类)。检查 XMLView,确认在控制器中调用 this.byId(...) 时实际获取的控件类型!
别忘了使用特定的事件类型,例如路由事件中的 Route$PatternMatchedEvent。
类型转换示例
不推荐:
(this.byId("form") as unknown as {setVisible: (v: boolean) => void}).setVisible(false);
推荐:
import SimpleForm from "sap/ui/layout/form/SimpleForm";
(this.byId("form") as SimpleForm).setVisible(false);
创建共享类型定义
你创建的许多类型定义可用于不同的文件。请在 src/types/ 等中央位置创建这些类型定义。
项目设置转换
1. package.json
如果 package.json 文件中尚未包含以下开发依赖项,则必须添加(非常重要):
{{dependencies}}
但是,如果 package.json 中已经存在某个依赖项,请勿提高其主版本号。 不要删除现有依赖项,只能添加新的配置。请尽早安装这些依赖项,以验证是否能找到相应类型。
重要:此外,你还必须将与 UI5 项目版本匹配的 @sapui5/types(或 @openui5/types)包作为开发依赖项添加。可在 ui5.yaml 中查找框架类型和版本,或使用 get_project_info MCP 工具获取。
另外,只有在依赖项或其版本发生变化时,才需确保(或告知用户)执行 npm install / yarn install(以项目中使用的工具为准),以安装已更改的依赖项。
typescript-eslint 依赖项仅适用于已配置 eslint 的项目(详情见下文)。
还要将 "ts-typecheck": "tsc --noEmit" 脚本添加到 package.json,以便你和开发者轻松检查 TypeScript 错误。
2. tsconfig.json
添加一个 tsconfig.json 文件。请参考以下示例,并根据当前项目的需要进行调整,例如调整 paths 映射:
{
"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
更新 ui5.yaml 文件,使其使用 ui5-tooling-transpile-task 和 ui5-tooling-transpile-middleware,并确保至少包含以下配置:
builder:
customTasks:
- name: ui5-tooling-transpile-task
afterTask: replaceVersion
server:
customMiddleware:
- name: ui5-tooling-transpile-middleware
afterMiddleware: compression
- name: ui5-middleware-livereload
afterMiddleware: compression
确保生成的 ui5.yaml 文件有效——避免重复条目,每个根配置只能存在一次。
如果 server 等配置已存在,必须在其中添加内容,而不是再添加一个条目。
4. Eslint 配置
仅当项目已设置 eslint 时,才扩展 eslint 配置,加入 TypeScript 专用部分。如果项目未在 package.json 中配置 eslint 依赖,也没有 eslint 配置文件,则无需进行任何操作。
完整的、兼容 eslint v9 的 eslint.config.mjs 文件可以如下所示,但实际内容取决于具体项目,因此你必须根据项目进行调整!
import eslint from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
globals: {
...globals.browser,
sap: "readonly"
},
ecmaVersion: 2023,
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname
}
}
},
{
ignores: ["eslint.config.mjs"]
}
);
应用代码转换
步骤 1:将专有的 UI5 类语法改为标准 ES 类语法
所有 UI5 类定义(SuperClass.extend(...))都必须转换为标准 JavaScript class。
UI5 类配置对象(extend 的第二个参数)中的属性将成为标准 JavaScript 类的成员。
重要的是,要在 JSDoc 注释中为类添加命名空间,以便反向转换时能够重新添加该命名空间。此 @namespace 注释必须紧邻类声明之前。
命名空间是完整包名和类名(extend 的第一个参数)中位于类名之前的部分。
转换前(示例):
[... 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());
}
});
转换后(示例,请勿直接照用此代码):
[... 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());
};
};
步骤 2:改用 ECMAScript 模块和导入
TypeScript UI5 应用必须使用现代 ES 模块和导入语法。
因此,请将所有 UI5 模块定义和依赖项加载调用(sap.ui.require(...)、sap.ui.define(...))
转换为 ES 模块和导入(对于 sap.ui.define,还需添加模块导出)。
以上面的示例为例,转换如下。
转换前:
sap.ui.define(["sap/ui/core/mvc/Controller"], function (Controller) {
/**
* @namespace ui5tssampleapp.controller
*/
class App extends Controller {
... // as above
};
return App;
});
转换后:
import Controller from "sap/ui/core/mvc/Controller";
/**
* @namespace ui5tssampleapp.controller
*/
export default class App extends Controller {
... // as above
};
sap.ui.require 应转换为仅包含导入、不包含导出的形式。
避免导入的模块名称冲突。
提示:导入
sap/ui/core/Core不会提供类(大多数其他 UI5 模块也是如此),而是提供 UI5 Core 的单例实例。因此,可以直接使用导入的模块调用byId(...)等方法,而不必调用sap.ui.getCore(),后者会在 JavaScript 中返回该单例实例。
如果动态使用 sap.ui.require,例如在方法体中使用 sap.ui.require(["sap/m/MessageBox"], function(MessageBox) { ... }),则应将其转换为动态导入,例如 import("sap/m/MessageBox").then((MessageBox) => { ... })。
步骤 3:标准 TypeScript 代码改造
运用你在将 JavaScript 代码转换为 TypeScript 方面的一般知识。具体来说:
- 在需要的地方为方法参数和变量添加类型信息。
- 在类定义的开头添加缺失的私有成员变量(并标注类型)。在 JavaScript 中,这些变量通常会在类实例的生命周期中途动态创建。
- 当使用
someFunction.bind(...)时,将常规的function转换为箭头函数,因为 TypeScript 似乎不会将绑定的this上下文类型传递到函数体中。 - 在适用的情况下,在代码中定义所需的其他类型和结构。
重要:使用 UI5 类型(例如为变量或方法参数/返回类型添加注解)时,不要使用带有全局命名空间的 UI5 类型(例如
sap.m.Button或sap.ui.core.Popup)!请改为从相应模块导入此 UI5 类型(例如sap/m/Button或sap/ui/core/Popup;如有需要,请添加导入语句),并使用导入的模块。
示例:
错误:
const b: sap.m.Button;
function getPopup(): sap.ui.core.Popup { ... }
正确:
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之类的类型!
示例:
之前:
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");
}
});
});
之后:
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。 - 这同样适用于所有类型,并非仅适用于事件。
步骤 4:为泛型方法的返回值添加类型断言
泛型 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,也不要将其断言为 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 方法。
之前:
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());
};
};
之后:
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 并不严格要求在方法返回类型中声明 void,但这样做有益处,例如可适配不同的 lint 配置。)
步骤 5:解决剩余问题
此时,剩余的 TypeScript 错误数量应该会大幅减少。 如果能明确识别出其中一些问题,请修复;如有疑问,请将最后剩余的问题告知开发者。
UI5 控件 TypeScript 转换指南
本节介绍如何将 UI5 自定义控件从 JavaScript 转换为 TypeScript。此指南适用于应用中的单个自定义控件以及控件库。
将 UI5 自定义控件转换为 TypeScript,除了遵循通用的 TypeScript 转换方式(转换专有 UI5 类和语法)外,还需要采用特定模式。
运行时生成的方法问题(重要)
这是最需要理解的一点。
UI5 会在运行时为所有属性、聚合、关联和事件生成 getter/setter(以及其他)方法。这意味着 TypeScript 在开发时无法识别这些方法,因而会产生类型错误。
问题
在元数据中定义了 text 属性的控件中:
static readonly metadata: MetadataOptions = {
properties: {
"text": "string"
}
};
尝试使用生成的方法时,TypeScript 会显示错误:
rm.text(control.getText()); // ERROR: Property 'getText' does not exist on type 'MyControl'
此外,TypeScript 不知道用于初始化控件的构造函数签名结构:
new MyControl("myId", {text: "Hello"}); // TypeScript doesn't know about the settings object structure
这会影响:
- 属性 getter/setter:
getText()、setText()、bindText() - 聚合方法:
addItem()、removeItem()、getItems(),…… - 关联方法:
getLabel()、setLabel() - 事件方法:
attachPress()、detachPress()、firePress() - 构造函数设置对象结构
解决方案:@ui5/ts-interface-generator
将接口生成器工具安装为开发依赖项:
npm install --save-dev @ui5/ts-interface-generator@{{ts-interface-generator-version}}
为方便后续开发,可在 package.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 后,运行一次生成器以生成所需的控件接口:
npm run watch:controls
这会生成一个 *.gen.d.ts 文件(例如 MyControl.gen.d.ts),其中包含具有所有运行时生成方法的 TypeScript 接口。TypeScript 会将这些接口与控件类合并。
这些生成的文件应提交到版本控制中,且不得手动编辑。
必需的构造函数签名(重要手动步骤)
运行接口生成器后,必须手动将终端输出中的构造函数签名复制到相应的控件类中。
生成器会输出类似如下内容:
===== 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 =====
将这些行复制到类体开头、元数据定义之前:
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 = {
// ...
};
}
控件元数据类型
控件元数据必须声明为 MetadataOptions 类型:
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 起提供
MetadataOptions;更早版本请使用object - 类型声明可防止继承控件时出现问题(不应重复声明继承的属性)
必需的命名空间注解
必须使用 @namespace JSDoc 注解,转换器才能生成正确的 UI5 类名:
/**
* @namespace ui5.typescript.helloworld.control
*/
export default class MyControl extends Control {
// ...
}
导出模式
定义类时必须立即使用 export default,否则 ts-interface-generator 会失败:
// CORRECT:
export default class MyControl extends Control {
// ...
}
// WRONG - separate export:
class MyControl extends Control {
// ...
}
export default MyControl;
元数据和渲染器的静态成员
元数据和渲染器都定义为 static 类成员:
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 代码:
sap.ui.define([
"sap/ui/core/Control",
"./MyControlRenderer"
], function (Control, MyControlRenderer) {
"use strict";
return Control.extend("com.myorg.myapp.control.MyControl", {
...
renderer: MyControlRenderer,
...
转换为以下 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;
...
完整控件示例
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(之后):
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();
}
}
库专用指南
转换整个控件库(而不只是应用中的单个控件)时,需要执行额外步骤:
含枚举的库模块(对于避免 XSS 问题至关重要!)
在 library.ts 中,必须将枚举附加到全局库对象,以确保 UI5 运行时兼容性:
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;
为什么库中的每个枚举都至关重要:
- 控件属性将类型引用为全局名称:
type: "com.myorg.myui5lib.ExampleColor" - UI5 运行时需要通过此全局路径查找枚举
- 如果没有此设置,UI5 将无法验证属性类型
- 这会破坏类型检查,并可能造成 XSS 漏洞,因为未经检查的内容可能会意外写入 HTML
在 tsconfig.json 中进行路径映射
对于库,请为库命名空间添加路径映射:
{
"compilerOptions": {
"paths": {
"com/myorg/mylib/*": ["./src/*"]
}
}
}
控件转换检查清单
将控件从 JavaScript 转换为 TypeScript 时:
- 像常规 UI5 模块一样转换为 ES6 类/模块
- 添加
@namespaceJSDoc 注释 - 在类定义处立即使用
export default - 将元数据类型定义为
MetadataOptions(从适当的基类导入) - 将元数据和渲染器定义为
static成员 - 安装并运行
@ui5/ts-interface-generator - 将构造函数签名从生成器输出复制到类中
- 如果位于库中:手动将枚举附加到全局库对象
- 保留所有 JSDoc 注释和文档
测试转换
将 UI5 测试代码从 JavaScript 转换为 TypeScript 时,有一些关键但不易察觉的模式。需要转换测试时,请参阅测试转换文档了解详情。