UI5(SAPUI5/OpenUI5)プロジェクトをTypeScriptに変換するためのスキルです。
A skill for converting UI5 (SAPUI5/OpenUI5) projects to TypeScript.
How to convert a UI5 (SAPUI5/OpenUI5) project to TypeScript: general rules, project setup changes, code conversion, and test conversion (separate file).
You MUST preserve existing JSDoc, documentation and comments - never remove JSDoc or comments during the conversion. When converting to a class, add @namespace but keep ALL existing JSDoc.
Before:
/**
* 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);
},
});
After:
/**
* 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;
}
}
Carefully respect all guidelines in this document. Before each conversion step, consider all relevant details.
Convert step by step: TypeScript project setup first, then central files other files depend on, so typed versions are available for consumers. "allowJs": true in tsconfig.json allows semi-converted projects.
any typeFind the proper type or create an interface instead of any:
// BAD: (this.getOwnerComponent() as any).getContentDensityClass();
// GOOD:
(this.getOwnerComponent() as AppComponent).getContentDensityClass()
unknown castsImport and use actual UI5 control types. Inspect the XMLView to find which control type you get from this.byId(...). Use specific event types like 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);
Create shared types in a central location like src/types/.
Add the following dev dependencies if not already present:
{{dependencies}}
Do not increase existing major versions. Do not remove existing dependencies.
IMPORTANT: Also add @sapui5/types (or @openui5/types) matching the UI5 project version as dev dependency. Framework type and version from ui5.yaml or get_project_info MCP tool.
If dependencies changed, ensure npm install / yarn install is run. The typescript-eslint dependency is only relevant when the project already has eslint. Also add "ts-typecheck": "tsc --noEmit" script to package.json.
Add a tsconfig.json. Use this as reference, adapt paths to the project:
{
"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/**/*"]
}
Add ui5-tooling-transpile-task and 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
Avoid duplicate entries — add to existing server/builder sections if they exist.
Only when eslint is already set up, enhance it with TypeScript-specific parts. Example 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"]
}
);
Convert SuperClass.extend(...) to a standard class. Properties in the config object (second extend parameter) become class members. Annotate the class with @namespace in a JSDoc comment (it must immediately precede the class declaration) — the namespace is the part of the full name (first extend parameter) that precedes the class name.
Before:
var App = Controller.extend("ui5tssampleapp.controller.App", {
onInit: function _onInit() {
// apply content density mode to root view
this.getView().addStyleClass(this.getOwnerComponent().getContentDensityClass());
}
});
After:
/**
* @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());
}
}
Convert sap.ui.define(...) to ES imports + export default. Convert sap.ui.require(...) to imports (no export). Avoid name clashes between imported modules.
Before:
sap.ui.define(["sap/ui/core/mvc/Controller"], function (Controller) {
class App extends Controller {
// ... as above
}
return App;
});
After:
import Controller from "sap/ui/core/mvc/Controller";
/**
* @namespace ui5tssampleapp.controller
*/
export default class App extends Controller {
// ... as above
}
Dynamic sap.ui.require inside method bodies → dynamic import:
import("sap/m/MessageBox").then((MessageBox) => { /* ... */ });
Hint: importing
sap/ui/core/Coreprovides the singleton instance, not the class.
someFunction.bind(...) to arrow functions (TypeScript does not propagate the bound this type into the function body).IMPORTANT: Never use a UI5 type with its global namespace (like
sap.m.Button). Always import it from the module (likesap/m/Button) and use the imported name.
Wrong:
const b: sap.m.Button;
function getPopup(): sap.ui.core.Popup { /* ... */ }
Correct:
import Button from "sap/m/Button";
import Popup from "sap/ui/core/Popup";
const b: Button;
function getPopup(): Popup { /* ... */ }
Use UI5 control event types, not browser events like Event or MouseEvent — UI5 events are different:
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");
}
}
For any event XYZ of a UI5 control ABC, types
ABC$XYZEventandABC$XYZEventParametersare available.
Use the most specific type that provides all needed properties: KeyboardEvent/MouseEvent not Event for browser events; Button$PressEvent not sap/ui/base/Event.
Generic methods return the super-type of all possible types although in practice it will usually be a specific sub-type. Cast the return value to the specific sub-type when needed; derive the actual type from context. This often requires an additional import. Most prominently affected: core.byId()/view.byId(), control.getBinding(), ownerComponent.getModel(), event.getSource(), component.getRootControl(), this.getOwnerComponent().
For the app controller example above, this adds an import of the app's component (AppComponent) so the cast can be done — without it, getOwnerComponent() returns a sap.ui.core.Component, which does not have the getContentDensityClass method.
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());
}
}
Do not cast to a superclass when it's already the returned type. Avoid guessing — skip the cast if the actual type isn't clear.
At this point remaining TypeScript errors should be vastly reduced. Fix clearly recognizable ones. In case of doubt, mention the last remaining issues to the developer.
Converting custom UI5 controls requires specific patterns beyond the general conversion. This applies to single custom controls within applications and to control libraries.
This is the most important aspect to understand.
UI5 generates getter/setter (and more) methods for properties, aggregations, associations, and events at runtime. TypeScript cannot see them at development time. A control with a text property in its metadata will have getText()/setText() at runtime, but TypeScript errors on control.getText(). TypeScript also does not know the constructor's 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), and the constructor settings object.
npm install --save-dev @ui5/ts-interface-generator@{{ts-interface-generator-version}}
Add a script to package.json to make subsequent development easier:
{
"scripts": {
"watch:controls": "npx @ui5/ts-interface-generator --watch"
}
}
NOTE: if the tsconfig covering the controls is in a subdirectory or has a different name, use --config path/to/tsconfig.json.
After converting all controls, run the generator once:
npm run watch:controls
It generates *.gen.d.ts files with interfaces for all runtime-generated methods, which TypeScript merges with the control class. Commit these files; never edit them manually.
Copy the constructor signatures from the generator's terminal output into the beginning of the class body, before the metadata definition:
export default class MyControl extends Control {
// The following three lines were generated and should remain as-is to make TypeScript aware of the constructor signatures
constructor(id?: string | $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings);
constructor(id?: string, settings?: $MyControlSettings) { super(id, settings); }
static readonly metadata: MetadataOptions = {
// ...
};
}
The control metadata must be typed as MetadataOptions:
import type { MetadataOptions } from "sap/ui/core/Element";
export default class MyControl extends Control {
static readonly metadata: MetadataOptions = {
properties: {
"text": "string"
}
};
}
sap/ui/core/Element (or the closest base class: ManagedObject, Component); use import type (design-time only).object for earlier versions.The @namespace JSDoc annotation is required for the transformer to generate correct UI5 class names:
/**
* @namespace ui5.typescript.helloworld.control
*/
export default class MyControl extends Control {
// ...
}
Must use export default immediately — a separate export breaks ts-interface-generator:
// CORRECT:
export default class MyControl extends Control {
// ...
}
// WRONG:
class MyControl extends Control {
// ...
}
export default MyControl;
Both metadata and renderer are static class members. The renderer can be inline or in a separate file:
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();
}
}
When the renderer is in a separate file (common in libraries), it should stay separate — import it (import MyControlRenderer from "./MyControlRenderer";) and assign static renderer = MyControlRenderer;.
When converting entire control libraries (not just single controls in apps), additional steps are required.
In library.ts, enums must be attached to the global library object for UI5 runtime compatibility:
import ObjectPath from "sap/base/util/ObjectPath";
export enum ExampleColor {
Red = "Red",
Green = "Green",
Blue = "Blue"
}
// CRITICAL: Attach to global library object
const thisLib = ObjectPath.get("com.myorg.myui5lib") as {[key: string]: unknown};
thisLib.ExampleColor = ExampleColor;
Why this is critical for every enum in the library:
type: "com.myorg.myui5lib.ExampleColor".For libraries, add path mappings for the library namespace:
{
"compilerOptions": {
"paths": {
"com/myorg/mylib/*": ["./src/*"]
}
}
}
Convert to ES6 class/module with @namespace and immediate export default; type metadata as MetadataOptions; define metadata and renderer as static members; install and run @ui5/ts-interface-generator and copy the constructor signatures from its output; attach enums to the global library object if in a library; preserve all JSDoc.
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.
原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。