关于 ui5-typescript-conversion
ui5-typescript-conversion 是将 SAPUI5/OpenUI5 项目从 JavaScript 转为 TypeScript 的技能指南。内容涵盖项目设置、应用代码、控件类型处理,并要求保留现有文档。
主要功能
- 01分步配置 TypeScript 项目
- 02将 UI5 类转换为 ES 类
- 03转换为 ES 模块导入与导出
- 04使用具体 UI5 类型和事件类型
- 05为自定义控件生成接口
- 06保留注释与 JSDoc
适用场景
- 01将 UI5 应用转换为 TypeScript
- 02将控制器迁移为 ES 类语法
- 03为 UI5 事件和控件添加类型
- 04用生成的接口为自定义控件定义类型
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ ui5/ plugins-coding-agents --skill ui5-typescript-conversion安装后在 Claude Code 中输入/ui5-typescript-conversion确认已加载。
附带文件references/test_conversion.md
详细介绍
UI5 TypeScript 转换指南
如何将 UI5(SAPUI5/OpenUI5)项目转换为 TypeScript:通用规则、项目设置变更、代码转换和测试转换(单独文件)。
通用转换规则
保留所有注释
必须保留现有的 JSDoc、文档和注释——转换过程中绝不能移除 JSDoc 或注释。转换为类时,添加 @namespace,但保留所有现有的 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 () {
return Controller.prototype.getOwnerComponent.call(this);
},
});
转换后:
/**
* 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 {
return super.getOwnerComponent() as UIComponent;
}
}
认真细致
仔细遵循本文档中的所有指南。每个转换步骤开始前,都要考虑所有相关细节。
按步骤进行
按步骤转换:先设置 TypeScript 项目,再转换其他文件所依赖的核心文件,以便使用者可以使用带类型的版本。在 tsconfig.json 中设置 "allowJs": true,即可支持尚未完全转换的项目。
避免使用 any 类型
应查找合适的类型或创建接口,而不是使用 any:
// BAD: (this.getOwnerComponent() as any).getContentDensityClass();
// GOOD:
(this.getOwnerComponent() as AppComponent).getContentDensityClass()
避免转换为 unknown 类型
导入并使用实际的 UI5 控件类型。检查 XMLView,确定 this.byId(...) 返回的控件类型。使用具体的事件类型,例如 Route$PatternMatchedEvent。
// BAD: (this.byId("form") as unknown as {setVisible: (v: boolean) => void}).setVisible(false);
// GOOD:
import SimpleForm from "sap/ui/layout/form/SimpleForm";
(this.byId("form") as SimpleForm).setVisible(false);
创建共享类型定义
在 src/types/ 等中心位置创建共享类型。
项目设置转换
1. package.json
如果尚未添加,请添加以下开发依赖项:
{{dependencies}}
不要提高现有依赖项的主版本号。不要移除现有依赖项。
重要:还要添加与 UI5 项目版本匹配的 @sapui5/types(或 @openui5/types)作为开发依赖项。可通过 ui5.yaml 或 get_project_info MCP 工具获取框架类型和版本。
如果依赖项有变更,请确保运行 npm install / yarn install。只有项目已配置 eslint 时,才需要 typescript-eslint 依赖项。还要向 package.json 添加 "ts-typecheck": "tsc --noEmit" 脚本。
2. tsconfig.json
添加 tsconfig.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
添加 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
避免重复添加条目——如果已有 server/builder 部分,请在其中添加。
4. Eslint 配置
仅当项目已配置 eslint 时,才添加 TypeScript 专用配置。以下是 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 类语法
将 SuperClass.extend(...) 转换为标准的 class。配置对象(第二个 extend 参数)中的属性应转换为类成员。在 JSDoc 注释中为类添加 @namespace(该注释必须紧邻类声明之前)——命名空间是完整名称(第一个 extend 参数)中位于类名之前的部分。
转换前:
var App = Controller.extend("ui5tssampleapp.controller.App", {
onInit: function _onInit() {
// apply content density mode to root view
this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
}
});
转换后:
/**
* @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());
}
}
步骤 2:改用 ECMAScript 模块和导入
将 sap.ui.define(...) 转换为 ES 导入和 export default。将 sap.ui.require(...) 转换为导入(不导出)。避免导入的模块之间发生命名冲突。
转换前:
sap.ui.define(["sap/ui/core/mvc/Controller"], function (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 → 动态导入:
import("sap/m/MessageBox").then((MessageBox) => { /* ... */ });
提示:导入
sap/ui/core/Core会提供单例实例,而不是类。
步骤 3:标准 TypeScript 代码调整
- 根据需要为方法参数和变量添加类型信息。
- 在类定义开头添加缺失的私有成员变量(并指定类型)。(在 JavaScript 中,这些变量通常在实例的生命周期中动态创建。)
- 将
someFunction.bind(...)转换为箭头函数(TypeScript 不会将绑定后的this类型传递到函数体中)。 - 根据需要定义其他类型和结构。
重要:切勿使用带全局命名空间的 UI5 类型(如
sap.m.Button)。始终从模块导入类型(如sap/m/Button),并使用导入的名称。
错误:
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 控件事件类型,而不是 Event 或 MouseEvent 等浏览器事件——UI5 事件有所不同:
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");
}
}
对于 UI5 控件 ABC 的任意事件 XYZ,都有类型
ABC$XYZEvent和ABC$XYZEventParameters可用。
使用能提供所有所需属性的最具体类型:对于浏览器事件,使用 KeyboardEvent/MouseEvent,而不是 Event;使用 Button$PressEvent,而不是 sap/ui/base/Event。
步骤 4:转换泛型方法的返回值
泛型方法会返回所有可能类型的超类型,但实际上通常是某个特定子类型。需要时,将返回值转换为特定子类型;根据上下文推导实际类型。这通常需要额外导入。受影响最明显的方法有:core.byId()/view.byId()、control.getBinding()、ownerComponent.getModel()、event.getSource()、component.getRootControl()、this.getOwnerComponent()。
对于上面的应用控制器示例,需要导入应用的组件(AppComponent),以便进行类型转换;否则,getOwnerComponent() 返回的是不包含 getContentDensityClass 方法的 sap.ui.core.Component。
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());
}
}
如果返回类型已经是某个父类,则不要将其转换为该父类。避免猜测——如果实际类型不明确,就跳过类型转换。
步骤 5:解决剩余问题
此时,剩余的 TypeScript 错误应已大幅减少。修复能够明确识别的问题。如有疑问,请向开发者说明最后剩余的问题。
UI5 控件 TypeScript 转换指南
转换自定义 UI5 控件需要遵循一些通用转换之外的特定模式。此指南适用于应用中的单个自定义控件和控件库。
运行时生成方法的问题(重要)
这是最重要的一点。
UI5 会在运行时为属性、聚合、关联和事件生成 getter/setter(以及其他)方法。TypeScript 在开发时无法识别这些方法。元数据中包含 text 属性的控件在运行时会有 getText()/setText() 方法,但 TypeScript 会对 control.getText() 报错。TypeScript 也无法识别构造函数的 settings 对象结构。这会影响属性 getter/setter(getText、setText、bindText)、聚合方法(addItem、removeItem、getItems)、关联方法(getLabel、setLabel)、事件方法(attachPress、detachPress、firePress)以及构造函数的 settings 对象。
解决方案:@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 位于子目录中,或名称不同,请使用 --config path/to/tsconfig.json。
完成所有控件的转换后,运行一次生成器:
npm run watch:controls
生成器会生成 *.gen.d.ts 文件,其中包含所有运行时生成方法的接口,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 = {
// ...
};
}
控制元数据类型声明
控制元数据必须声明为 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导入(或从最接近的基类导入:ManagedObject、Component);使用import type(仅用于设计时)。 - 自 UI5 1.110 起可用;更早版本请使用
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:
class MyControl extends Control {
// ...
}
export default MyControl;
元数据和渲染器的静态成员
元数据和渲染器都是 static 类成员。渲染器可以内联定义,也可以放在单独的文件中:
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 {
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();
}
}
当渲染器位于单独的文件中(在库中很常见)时,应保持独立——导入它(import MyControlRenderer from "./MyControlRenderer";),并将其赋值给 static renderer = MyControlRenderer;。
库专属指南
转换整个控制库(而非应用中的单个控制)时,还需要执行其他步骤。
含枚举的库模块(对于避免 XSS 问题至关重要!)
在 library.ts 中,必须将枚举附加到全局库对象,以确保与 UI5 运行时兼容:
import ObjectPath from "sap/base/util/ObjectPath";
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 就无法验证类型 → 未经检查的内容可能会写入 HTML → 存在 XSS 漏洞。
tsconfig.json 中的路径映射
对于库,请为库命名空间添加路径映射:
{
"compilerOptions": {
"paths": {
"com/myorg/mylib/*": ["./src/*"]
}
}
}
控制转换检查清单
转换为带有 @namespace 的 ES6 类/模块,并立即使用 export default;将元数据声明为 MetadataOptions 类型;将元数据和渲染器定义为 static 成员;安装并运行 @ui5/ts-interface-generator,然后复制其输出中的构造函数签名;如果是库,则将枚举附加到全局库对象;保留所有 JSDoc。
测试转换
将 UI5 测试代码从 JavaScript 转换为 TypeScript 时,有一些关键但不明显的模式。测试需要转换时,请参阅测试转换文档了解详情。