mcpField
U
ui5-typescript-conversion

ui5-typescript-conversion

github.com/ui5開発45201 回インストール2 週間前に更新
リポジトリ

ui5-typescript-conversion について

ui5-typescript-conversionは、SAPUI5/OpenUI5プロジェクトをJavaScriptからTypeScriptへ移行するためのガイドスキルです。プロジェクト設定、アプリコード、型付きコントロールを扱い、既存の文書も保持します。

主な機能

  1. 01段階的なTypeScriptプロジェクト設定
  2. 02UI5クラスからESクラスへの変換
  3. 03ESモジュールのインポートとエクスポート
  4. 04具体的なUI5型とイベント型の利用
  5. 05カスタムコントロール用インターフェース生成
  6. 06コメントとJSDocの保持

活用シーン

  1. 01UI5アプリケーションのTypeScript化
  2. 02コントローラーのESクラス構文への移行
  3. 03UI5イベントやコントロールへの型追加
  4. 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 やコメントを削除しないでください。クラスに変換する際は、既存の JSDoc をすべて残したまま @namespace を追加します。

変換前:

/**
 * 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}}

既存のメジャーバージョンを上げないでください。既存の依存関係を削除しないでください。

重要:@sapui5/types(または @openui5/types)も、UI5 プロジェクトのバージョンに合わせて開発依存関係として追加してください。フレームワークの型とバージョンは、ui5.yaml または get_project_info MCP ツールで確認します。

依存関係を変更した場合は、npm install / yarn install が実行されるようにしてください。typescript-eslint 依存関係が関係するのは、プロジェクトですでに eslint を使用している場合のみです。また、"ts-typecheck": "tsc --noEmit" スクリプトを package.json に追加してください。

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 に変換します。設定オブジェクト(2 番目の extend 引数)のプロパティはクラスメンバーになります。JSDoc コメントでクラスに @namespace を指定します(クラス宣言の直前に記述する必要があります)。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 { /* ... */ }

ブラウザーイベント(Event や MouseEvent など)ではなく、UI5 コントロールのイベント型を使用してください。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 が用意されています。

必要なプロパティをすべて備えた、最も具体的な型を使用してください。ブラウザーイベントには Event ではなく KeyboardEvent/MouseEvent を、sap/ui/base/Event ではなく Button$PressEvent を使用します。

ステップ 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 は、プロパティ、集約、関連付け、イベント用のゲッター/セッター(およびその他のメソッド)を実行時に生成します。TypeScript は開発時にこれらを認識できません。メタデータに text プロパティがあるコントロールでは、実行時に getText()/setText() が使用できますが、TypeScript は control.getText() でエラーを出します。また、TypeScript はコンストラクターの設定オブジェクトの構造も認識しません。この問題は、プロパティのゲッター/セッター(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 がサブディレクトリにある場合、または名前が異なる場合は、--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 を使用してください。
  • 型を指定することで、コントロールを継承する際の問題を防げます(継承したプロパティを再度記述する必要はありません)。
名前空間アノテーションが必須

トランスフォーマーで正しい UI5 クラス名を生成するには、@namespace JSDoc アノテーションが必須です。

/**
 * @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 に変換する際には、見落としやすい重要なパターンがあります。テストを変換する必要がある場合は、詳細についてテスト変換ドキュメントを参照してください。

コメント