experience-lwc-typescript-migrate について
Lightning Web Components(LWC)を JavaScript から TypeScript に移行する Salesforce 開発者向けのスキルです。Git の履歴を保ったリネームと、公開された @api メンバーのみを含む .d.ts の生成を行い、コンパイルとテストで確認します。
主な機能
- 01コンポーネントと補助ファイルを .ts に変更
- 02公開・内部メンバーへの型付け
- 03@api メンバーのみの .d.ts 生成
- 04イベントと非同期メソッドの型指定
- 05TypeScript コンパイルと Jest テストの実行
活用シーン
- 01単一の LWC を JavaScript から TypeScript に移行
- 02既存 LWC の公開型宣言を作成
- 03リネーム済みコンポーネントに型を追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ forcedotcom/ sf-skills --skill experience-lwc-typescript-migrateインストール後、Claude Code で/experience-lwc-typescript-migrateを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/dts-template.tstype-patterns.tsscripts/find-consumers.sh詳細
LWC を TypeScript に変換する
Lightning Web Component バンドルを JavaScript から TypeScript に変換します。成果物は、型が完全に付けられた .ts 実装と、@api メンバー(他の LWC が利用する公開 API)のみを公開する .d.ts ファイルです。
このスキルを使うタイミング
- 単一のコンポーネントまたはコンポーネントのフォルダーを
.jsから.tsに移行したい場合。 - 既存の LWC 用に
.d.tsが必要で、他のコンポーネント(または外部の TypeScript ホスト)から安全にインポートできるようにしたい場合。 - すでに
.tsに名前を変更したものの、適切に型が付けられていない LWC に型注釈を追加したい場合。 - JSDoc 形式の型ヒントを、実際の TypeScript の型にアップグレードしたい場合。
前提条件
- 現在、コンポーネントが JavaScript で正しくビルドされ、動作していること。
gitが利用可能であること(名前の変更時にgit mvを使い、履歴を保持する必要があります)。- TypeScript コンパイラーがビルドに組み込まれていること(SFDX TS パイプライン、またはスタンドアロンの
tscステップ)。
ワークフロー
ステップ 1 — コンポーネントを確認する
バンドル内のすべてのファイルを開きます。
componentName/
├── componentName.js
├── componentName.html
├── componentName.css
└── (possibly) __tests__/, __utam__/, existing .d.ts
次の点を確認します。
LightningElementを継承しているものは何ですか? クラス名は何ですか?@apiデコレーターが付いているフィールドとメソッドはどれですか?- 既存の JSDoc が付いているプロパティやメソッドはありますか?(型ヒントの出発点として使いますが、実際の使用状況と照らし合わせて検証してください。JSDoc が誤っていることもあります。)
- コード内での呼び出し方から、どのパラメーターや戻り値の型を推測できますか?
ステップ 2 — git mv を使って .js を .ts に名前変更する
git mv componentName/componentName.js componentName/componentName.ts
バンドル内にヘルパーの .js ファイルがあれば、同様に名前を変更します(すでに .ts の場合を除く)。決して通常の mv を使わないでください。TypeScript のレビュアーが頼りにしている履歴とのつながりが失われます。
ステップ 3 — .ts に型注釈を追加する
公開 API の契約が確かなものになった時点で作業を止められるよう、次の優先順位で型を適用します。
- まず
@apiプロパティとメソッドに型を付けます。 JSDoc がなければ追加し、JSDoc の型を TypeScript の構文(string、number、boolean、Promise<T>)に変換します。JSDoc の記述を信頼する前に、コードと照らし合わせて検証してください。 - 複雑な形状には
interfaceまたはtypeエイリアスを使います。 同じインラインの型をあちこちで繰り返さないでください。 - オプショナルメンバーには
?を使います。 ただし、値が実際にundefinedになり得る場合に限ります。用心のためだけに?を付けないでください。 - プライベート/内部状態にも型を付けますが、その型はエクスポートしません。 利用側から決して触れられてはならないメンバーには
privateを使います。 - イベントハンドラー — 正確な DOM イベント型を優先します。
onclick(およびその他のクリック系ハンドラー)にはMouseEventを使います。キーボード操作によるクリックを含め、clickはMouseEventとしてディスパッチされます。そのため、PointerEventとして型を付けると、ハンドラーがその場合にはundefinedとなるポインター専用フィールド(pointerType、pressureなど)に依存できてしまいます。- ポインター固有のフィールドが実際に意味を持つ
onpointerdown/onpointerup/onpointermoveやその他のpointer*ハンドラーにはPointerEventを使います。 - LWC のカスタムイベントには
CustomEvent<{ detail: ... }>を使います。 Eventは最終手段とし、使用する場合はその理由を記載してください。
- 非同期メソッドは常に
Promise<T>を返します。Tだけを返すことはありません。 anyは避けます。 本当に型を付けられない場合はunknownを使い、型ガードで絞り込みます。
参考パターン
[[assets/type-patterns.ts|assets/type-patterns.ts]] を読み込み、プロパティ、メソッド、イベントハンドラーの型を示すインラインの例として使います。
ステップ 4 — .d.ts の生成
.ts の隣に componentName.d.ts を作成します。次の条件を満たす必要があります。
@apiメンバーのみを含めること — プライベートな状態、内部メソッド、それ自体が@apiでない限りライフサイクルフックは含めません。- 各宣言の直前に
@apiJSDoc をそのまま保持すること(@type、@required、@default、@param、@returnsタグを含む)。 - LWC モジュール名前空間
c/componentName(または異なる場合は組織の名前空間)を宣言すること。
テンプレート: [[assets/dts-template.ts|assets/dts-template.ts]] を開始時の .d.ts の形として読み込みます。
コンポーネントに @api メンバーがない場合でも、公開サーフェスがないことを説明するコメントとともにモジュール宣言を生成してください — ファイルをスキップしないでください。
ステップ 5 — コンパイルとテスト
- TypeScript コンパイラ(
tsc --noEmitまたはビルドの同等のもの)を実行します。完了とする前にすべてのエラーを解決してください。@ts-ignoreによる回避は禁止です。 - コンポーネントの既存の Jest テストを実行します。動作は同一であるべきです。
- 同梱の consumer-finder を無条件で実行します — 出力が空でも有効な結果であり、スキップする理由にはなりません。このスクリプトは
sfdx-project.jsonのpackageDirectories(またはフォールバックとして<project-root>)から検索パスを解決し、プロジェクトルートを脱出するエントリを拒否し、LWC インポートの検索を内部で実行するため、呼び出しは完全に決定論的です。
"<skill_dir>/scripts/find-consumers.sh" "<project-root>" "<componentName>"
各一致について、コンシューマーが期待する型が新しい .d.ts の公開サーフェスと依然として整合していることを確認します。
ステップ 6 — 最終的なバンドルの想定形状
componentName/
├── componentName.ts # Main TypeScript implementation
├── componentName.html # Template (unchanged)
├── componentName.css # Styles (unchanged)
└── componentName.d.ts # Type definitions (new)
検証チェックリスト
変換前:
- コンポーネントが有効な JS であり、すべてのテストがパスする。
- すべての
@apiメンバーとその意図する型を特定済みである。
変換後:
- 履歴を保持するために
git mvを使用した。 -
.ts内のすべての変数とパラメーターが具体的な型を持つ(暗黙のanyがない)。 - 複雑なオブジェクト形状は
interface/typeエイリアスにあり、インラインで繰り返されていない。 - オプションの
?は本当にオプションのフィールドにのみ付いている。 -
.d.tsが存在し、c/componentNameを宣言し、LightningElementを継承し、@apiメンバーのみを含む。 - すべての
@apiJSDoc が.d.tsにそのまま保持されている。 -
tscがエラーゼロでパスする。回避策として@ts-ignoreやanyを使用していない。 - Jest テストが依然としてパスする。
よくある落とし穴
- エラーを黙らせるために
anyを使う。 実際の型を解決してください。値が本当に未知の場合は、unknown+ 型ガードを使用します。 .d.tsにプライベートメンバーを含める。.d.tsは公開契約です。内部のライフサイクルとヘルパーが漏れてはいけません。- リネーム中に JSDoc を失う。 前後でスキャンしてください —
@apiメンバーの JSDoc コメントは.tsと.d.tsの両方に現れる必要があります。 git mvをスキップする。 レビューが悲惨になり、blame が混乱します。- async の戻り値の型を忘れる。
asyncキーワードを伴うfoo()は常にPromiseを返します。宣言してください。 onclickをPointerEventと型付けする。clickはMouseEvent(キーボードでトリガーされたクリックを含む)であるため、pointerTypeのようなPointerEventのフィールドはそれらのイベントでは undefined になります。onclickはMouseEventと型付けし、PointerEventはonpointer*ハンドラー用に取っておいてください。MouseEvent | TouchEventは、コードがTouchEventで明確に分岐する場合にのみ使用します。