• Projects
  • Service
  • About
  • branding.bz
  • Podcast
  • Tips
  • FAQ
  • Recruit
  • Download
  • Contact
  • branding.bz(ブランド構築SaaS)
  • DESIGN NOW(デザインメディア)
  • X
  • LinkedIn
  • Spotify
  • Facebook

213-0011 神奈川県川崎市高津区久本3-6-7-303

© 2026 ID INC. All rights reserved

claude-skills/スキル
SKILLOfficialdevelopment

ui5-typescript-conversion

プラグイン
ui5-typescript-conversion
ソース
GitHub で見る ↗
説明

UI5(SAPUI5/OpenUI5)プロジェクトをTypeScriptに変換するためのスキルです。

原文を表示

A skill for converting UI5 (SAPUI5/OpenUI5) projects to TypeScript.

ユースケース
  • UI5プロジェクトをTypeScriptに変換する
  • JavaScriptからTypeScriptへの移行
本文

UI5 TypeScript Conversion Guidelines

How to convert a UI5 (SAPUI5/OpenUI5) project to TypeScript: general rules, project setup changes, code conversion, and test conversion (separate file).

General Conversion Rules

Preserve ALL comments

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

Be diligent

Carefully respect all guidelines in this document. Before each conversion step, consider all relevant details.

Go step-by-step

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.

Avoid any type

Find the proper type or create an interface instead of any:

// BAD: (this.getOwnerComponent() as any).getContentDensityClass();
// GOOD:
(this.getOwnerComponent() as AppComponent).getContentDensityClass()

Avoid unknown casts

Import 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 type definitions

Create shared types in a central location like src/types/.

Project Setup Conversion

1. package.json

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.

2. tsconfig.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/**/*"]
}

3. ui5.yaml

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.

4. Eslint configuration

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"]
	}
);

Application Code Conversion

Step 1: Change UI5 class syntax to ES class syntax

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

Step 2: Change to ECMAScript modules and imports

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/Core provides the singleton instance, not the class.

Step 3: Standard TypeScript Code Adaptations

  • Add type information to method parameters and variables where needed.
  • Add missing private member class variables (with types) to the beginning of the class definition. (In JavaScript they are often created on-the-fly during the instance lifetime.)
  • Convert someFunction.bind(...) to arrow functions (TypeScript does not propagate the bound this type into the function body).
  • Define further types and structures as needed.

IMPORTANT: Never use a UI5 type with its global namespace (like sap.m.Button). Always import it from the module (like sap/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$XYZEvent and ABC$XYZEventParameters are 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.

Step 4: Casts for Return Values of Generic Methods

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.

Step 5: Solving any Remaining Issues

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.

UI5 Control TypeScript Conversion Guidelines

Converting custom UI5 controls requires specific patterns beyond the general conversion. This applies to single custom controls within applications and to control libraries.

The Runtime-Generated Methods Problem (CRITICAL)

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.

The Solution: @ui5/ts-interface-generator

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.

Required Constructor Signatures (CRITICAL MANUAL STEP)

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 = {
        // ...
    };
}

Control Metadata Typing

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"
        }
    };
}
  • Import from sap/ui/core/Element (or the closest base class: ManagedObject, Component); use import type (design-time only).
  • Available since UI5 1.110; use object for earlier versions.
  • Typing prevents issues when inheriting from the control (inherited properties should not be repeated).

Namespace Annotation Required

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 {
    // ...
}

Export Pattern

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;

Static Members for Metadata and Renderer

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;.

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

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:

  • Control properties reference types as global names: type: "com.myorg.myui5lib.ExampleColor".
  • The UI5 runtime needs to find the enum via this global path to validate the property type.
  • Without the attachment, UI5 cannot validate the type → unchecked content can be written to HTML → XSS vulnerability.

Path Mapping in tsconfig.json

For libraries, add path mappings for the library namespace:

{
    "compilerOptions": {
        "paths": {
            "com/myorg/mylib/*": ["./src/*"]
        }
    }
}

Control Conversion Checklist

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.

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.

原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。