LWCでTypeScriptの型定義を使う
こんにちは。木村です。
もう世の中的にもフロントエンドはTypeScriptで実装するのが一般的になっていると思います。それに、TypeScriptでの開発に慣れてしまうとJavaScriptで実装するのが不安だし面倒になってきますよね。
となると、Lightning Web コンポーネント(LWC)もTypeScriptで開発がしたい!と思ってしまいます。
しかし、現実問題としてそれはなかなか難しいです。折衷案としてSalesforce Developer BlogでJavaScriptで実装するが、型定義だけでも使おうというアプローチが紹介されています。
TypeScriptがJSDocのアノテーションによる型定義/型指定をサポートしており、この方法はその仕組みを利用しています。公式ドキュメントは以下になります。VSCodeもこの書き方をサポートしており、JSDocで型定義/指定を書くと補完してくれるようになります。
実際弊社製品の一部機能に導入してみましたが、TypeScriptで実装できないとは言え、VSCodeで入力補完や型の不一致によるエラーが出るようになり、これだけでもかなり便利になります。
これらの記事で基本的なやり方は理解できるのですが、いざ実務に使おうとするとはまるところが多かったので、ちゃんと実務で使えるように説明していきたいと思います。最初に基本的な導入手順、その後にはまりどころをQ&A形式でまとめました。
JSDocアノテーションでの型の導入手順
例としてSalesforce公式サンプルアプリであるebikes-lwcのheroDetails.jsに型を追加していきます。
1. lwc/jsconfig.jsonに「"target":"ESNext"」を追加
これをしないとget/setなどES5以降の機能を使っているとエラーになってしまいます。
お好みですが、「"noImplicitAny":true」も追加しておきましょう。設定しないと暗黙のany型がPROBLEMSに表示されず、型指定漏れに気づきにくいです。
"compilerOptions": {
"experimentalDecorators": true,
"target": "ESNext",
"noImplicitAny": true
},
2. 型チェックを導入したいLWCのJSファイルの先頭に「// @ts-check」を追加。
そうするとこのファイルが型チェックされるようになり、PROBLEMSタブに型チェックのエラーが出力されます。全ファイルに型チェックを強制する方法は後述します。
3. エラー箇所を右クリックしてInfer all types from usage
暗黙のany型になっている箇所を右クリックして「Infer all types from usage」をクリックすると、利用状況から推測した型定義を追加してくれます。
実行すると以下のようにJSDocアノテーションで型を宣言してくれます。
まだエラーが出ていますし、多くの型指定がany型になってしまっていて意味がありません。
4. 手動で型を指定していく
ここからはがんばって手動で型を指定していきます。
完成形を貼ると以下のようになります。
// @ts-check
import { LightningElement, api, wire } from 'lwc';
import getRecordInfo from '@salesforce/apex/ProductRecordInfoController.getRecordInfo';
/**
* @typedef GetRecordInfoResponse
* @property { { body: { message: string } } } error
* @property { string[] } data
*/
/**
* Details component that is on top of the video.
*/
export default class HeroDetails extends LightningElement {
/**
* @type {string}
*/
@api title = 'Hero Details'; // Default title to comply with accessibility
/**
* @type {string}
*/
@api slogan;
/**
* @type {string}
*/
@api recordName;
/**
* @type {GetRecordInfoResponse}
*/
recordInfoData;
/**
* @type {string}
*/
hrefUrl;
@wire(getRecordInfo, { productOrFamilyName: '$recordName' })
recordInfo(/** @type {GetRecordInfoResponse} */ { error, data }) {
this.recordInfoData = { error, data };
// Temporary workaround so that clicking on button navigates every time
if (!error && data) {
if (data[1] === 'Product__c') {
this.hrefUrl = `product/${data[0]}`;
} else {
this.hrefUrl = `product-family/${data[0]}`;
}
}
}
}
any型だったsloganなどは実際の使われ方を見て期待している型を指定していきます。
ここでちょっと難しいのはApexメソッドのレスポンスであるrecordInfoDataです。実際のProductRecordInfoControllerを見ると戻り値はList<String>であることがわかるので、dataは「string[]」になります。Apexメソッドのエラーは(残念ながら経験的にしかわからないのですが)「{ body: { message : string } }」のような型になります。
したがって、getRecordInfoの戻り値の型として以下のような新しい型を定義すればよいです。型は@typedefアノテーションで定義できます。
/**
* @typedef GetRecordInfoResponse
* @property { { body: { message : string } } } error
* @property { string[] } data
*/
このGetRecordInfoResponse型を2箇所の使用箇所である
/**
* @type {GetRecordInfoResponse}
*/
recordInfoData;
と
recordInfo(/** @type {GetRecordInfoResponse} */ { error, data }) {
で、指定します。コールバック関数の引数の型は上記のように記述します。
5. 入力補完が効くようになります
これで以下のように入力補完が効くようになります。便利!
型定義をTypeScriptで記述する手順
上記手順はJSDocアノテーションだけを使用しました。しかし、@typedefアノテーションは特殊な記法です。型定義をTypeScriptで記述することもできるので、その手順をやってみましょう。
1. 型定義を作成
以下の内容で force-app/main/default/lwc/@types/apex.d.ts を作成します。上の手順で@typedefアノテーションで定義していた型を再定義しています。
/**
* Apexエラー
*/
interface ApexError {
body: { message: string }
}
/**
* ProductRecordInfoController.getRecordInfoの戻り値
*/
interface GetRecordInfoResponse {
error: ApexError
data: string[]
}
ディレクトリ名や場所もファイル名も何でもいいのですが、@をつけると先頭に来てわかりやすいので、このディレクトリ名にしています。
jsconfig.jsonで"**/*"をincludeしているので、lwcの下のディレクトリにすると自動的に読み込んでくれます。
また、このディレクトリを.forceignoreに忘れずに追加しておきましょう。pushのときにエラーになってしまいます。
# 型定義
force-app/main/default/lwc/@types
2. LWCで作成した型定義を使うように書き換える
同じインターフェース名にしたので、アノテーションを使用した型定義を削除するだけで大丈夫です。
// @ts-check
import { LightningElement, api, wire } from 'lwc';
import getRecordInfo from '@salesforce/apex/ProductRecordInfoController.getRecordInfo';
/**
* Details component that is on top of the video.
*/
export default class HeroDetails extends LightningElement {
/**
* @type {string}
*/
@api title = 'Hero Details'; // Default title to comply with accessibility
/**
* @type {string}
*/
@api slogan;
/**
* @type {string}
*/
@api recordName;
/**
* @type {GetRecordInfoResponse}
*/
recordInfoData;
/**
* @type {string}
*/
hrefUrl;
@wire(getRecordInfo, { productOrFamilyName: '$recordName' })
recordInfo(/** @type {GetRecordInfoResponse} */ { error, data }) {
this.recordInfoData = { error, data };
// Temporary workaround so that clicking on button navigates every time
if (!error && data) {
if (data[1] === 'Product__c') {
this.hrefUrl = `product/${data[0]}`;
} else {
this.hrefUrl = `product-family/${data[0]}`;
}
}
}
}
型定義をTypeScriptで定義できて、見通しがよくなりましたね。
よくありそうな質問
上記手順で基本は理解できると思います。この後は疑問に思いそうなところやはまりどころをQ&A形式で解説していきます。
全JSファイルに型チェックを強制する方法は?
jsconfig.jsonに"checkjs": trueを追加するだけです。
"compilerOptions": {
"experimentalDecorators": true,
"checkJs": true,
"target": "ESNext",
"noImplicitAny": true
},
LightningElementなどの型定義はどこにあるの?
LightningElementのような標準クラスは型を指定しなくてもエラーがでませんね。
これは以下のようなLWC開発に必要な基本的な型定義はVSCodeのエクステンションがVSCodeでプロジェクトを開いたときに .sfdx/typings に作成してくれているためです。
LWCの標準モジュールのインターフェース
lightning/ui*Api用の標準オブジェクトのインターフェース
このプロジェクトで定義しているApexクラスのインターフェース
このプロジェクトで静的リソースのインターフェース
LWC内でconとか入力すると候補を出してくれると思いますが、この機能は上記の型定義を使って動いています。
カスタムオブジェクトの型定義は自動生成してくれないの?
LightningElementなどの型定義はどこにあるの?で説明しましたが、lightning/ui*Api用の標準オブジェクトの型定義は自動生成してくれますが、カスタムオブジェクトや標準オブジェクトのカスタム項目やの型定義は生成してくれません。
以下のようなコードはエラーになってしまいます。
そのため、面倒ですが、必要に応じて以下のような型定義を作成する必要があります。
declare module "@salesforce/schema/Product__c" {
const objectApiName: string;
export default {
objectApiName
}
}
declare module "@salesforce/schema/Product__c.Name" {
const Name: string;
export default Name;
}
LightningModalやlightning-inputなどの型定義はないの?
VSCodeエクステンションの開発が追いついていないのか、新し目のモジュールは型定義がありません。自作する必要があります。
Ziemniakoss/lwc-typings-generator というリポジトリでこの辺りの型を定義してくれているのを見つけました。ここから必要な定義を持ってきたり、参考に定義するのもよいと思います。
querySelectorがうまく型定義できません
querySelectorの型定義ではElementが返ってくるよう定義されているのですが、LWCの場合はコンポーネントが返ってくることがよくあります。
例えば、以下のようなコードがあるとします。
let input = this.template.querySelector('lightning-combobox');
input.focus();
その場合は以下のようにlightning/comboboxの型定義をし、(先ほどのlwc-typings-generatorから拝借しています。)
declare module "lightning/combobox" {
export default class Combobox {
blur();
focus();
checkValidity(): boolean;
setCustomValidity(message: string);
reportValidity(): boolean;
}
}
以下のように型指定するとうまくいきます。
/** @type { Element & Combobox } */
let input = this.template.querySelector('lightning-combobox');
input.focus();
Comboboxの定義でextends Elementして@type { Combobox }としてしまってもよいと思います。
カスタムコンポーネントをimportするとエラーになります
以下のようなケースです。c/ldsUtilsをインポートするところでCannot find module 'c/ldsUtils'. とエラーになっています。
このときはjsconfig.jsonに以下のように"baseUrl":"./"と"paths"の設定を追加するとエラーが解消されます。
"compilerOptions": {
"experimentalDecorators": true,
"target": "ESNext",
"noImplicitAny": true,
"baseUrl": "./",
"paths": {
"c/ldsUtils": ["ldsUtils/ldsUtils"]
}
},
LWCのディレクトリ構成だと、ldsUtilsディレクトリの中にldsUtils.jsファイルがあって、それをc/ldsUtilsモジュールとしてインポートできるため、それを解決するためにはこう書くしかなさそうです。
importが必要なカスタムコンポーネントを増やす度に追加しないといけないので、そこが大変面倒です。
いい方法があったら教えてください。
ファイルを開かないとPROBLEMSにエラーが表示されない?
なんかそんな気がします。全部のチェックをするには以下のようなコマンドを実行する必要がありそうです。
tsc -w -p force-app/main/default/lwc/jsconfig.json
いい方法があったら教えてください。
CIで実行するには?
まだ試していませんが、.sfdx/typingsもVSCodeのエクステンションがないと生成されないので、CIで実行するのは難しいかもしれません。
.sfdx/typingsをコミットしちゃうといいですかね。
いい方法があったら教えてください。
おわりに
なかなかに面倒なところはあるのですが、それ以上に型によるある程度の安全性と開発効率のよさを享受できるので、導入してみた感じはけっこうよさそうだと感じています。
自分のはまったところの解説も結構書いたので、スムーズな導入の助けになれば幸いです。
本当はTypeScriptで実装したいですけど、それは茨の道すぎるので、その道は今後のSalesforceのエンハンスもしくはType Annotations ProposalがJavaScriptに採用されることに期待したいところです。
なお、この記事は昨日開催した「ゆるっとSalesforceトーク #14 」で発表した内容をまとめた記事になります。毎月第2水曜日に開催していますので、よろしければグループメンバーになって次回の開催通知をお待ちください。