はじめに
いつもと違う環境でアプリを作りたくなり、Vue.js+TypeScriptでアプリを作ってみました。
Vueのみ or TypeScriptのみの情報は結構見つかりましたが、Vue+TypeScriptについてがっつり書いてるものが無かったので自分で記事を書いてみることにしました。
この記事では分かりやすいようにコードを比較する形で書いてます。
※JavaScriptのコード例はTypeScriptから逆算して書いたので間違ってるかもしれません。その際は指摘してもらえると助かります。
勧める理由
VueとTypeScript(正確にはvue-class-component
やvue-property-decorator
も含む)を併用することで、型システムやデコレーター等を用いて型安全なコードを簡潔に書けるためです。TypeScriptはいいぞ。
作ったもの
今回はCoD(FPSの洋ゲー)の戦績閲覧アプリをElectronで作りました。
アカウントを入力すると戦績が表示されます。モード毎の戦績とマップごとの戦績があり、モードやマップを絞り込んだり戦績順にソートしたりできます。また下にはサマリ行があり、カーソルを重ねると円グラフがポップアップします。
Qiita用 pic.twitter.com/WWiodgQ3ML
— なつ@VR (@VRNatsuVR) 2019年3月3日
TypeScriptでのVueクラスの書き方
まずは(トランスパイラを使わない)JavaScriptとTypeScriptを比較してみましょう。以下は単一ファイルコンポーネントのスクリプト部分です。
まずはJavaScript。
module.exports = {
// 型と初期値を持つprops
props: {
propMessage: {
type: String,
default: ""
}
}
data: function() {
return {
message: 123 // 初期データ
}
}
// 算出プロパティ
computed: {
computedMessage: function() {
return 'computed ' + this.message;
}
}
// 監視プロパティ
watch: {
message: {
handler: function(newMessage, oldMessage) { /*処理*/ },
immediate: true,
deep: true
}
}
// ライフサイクルフック
mounted: function() {
this.greet();
}
// 普通のメソッド
methods: {
greet: function() {
alert('greeting: ' + this.message);
}
}
}
次にTypeScript。
import Vue from 'vue'
import Component from 'vue-class-component'
@Component({
// 型と初期値を持つpropsと監視プロパティ
props: {
propMessage: {
type: String,
default: ""
}
},
watch: {
"message": [
{
handler: "onMessageChanged",
immediate: true,
deep: true
}
]
},
methods: {
onMessageChanged(newMessage: string, oldMessage: string): string { /*処理*/ }
}
})
export default class App extends Vue {
// 初期データをクラスのプロパティとして記述できる
message: string = 123;
// 算出プロパティは読み取り専用プロパティとして記述できる
get computedMessage(): string => 'computed ' + this.message;
// ライフサイクルフックをクラスメソッドとして記述できる
mounted(): void => this.greet();
// 普通のメソッドもクラスメソッドとして記述できる
greet(): void => alert('greeting: ' + this.message);
}
上の例で分かるかもしれませんが、JavaScriptでは以下のような制約がありコードを簡潔に書けません。
-
data
をメソッドの戻り値として定義する必要がある -
watch
、methods
、ライフサイクルフック(mounted
等)でアロー演算子が非推奨
TypeScriptではdata
はクラスのプロパティで書けますし、アロー演算子も使えるため簡潔にコードを書けます。更にプロパティ・メソッドの引数や戻り値に型を指定することができ、型安全にコードを書けます。
Vue Property Decoratorを用いた記述
TypeScriptはJavaScriptと比較して簡潔にコードが書けることがわかったかと思いますが、propsとwatchが少し冗長と思ったかも知れません。それを解決するのがvue-property-decoratorです。
vue-property-decorator
を導入することで、props
とwatch
は以下のように書き換えることが出来ます。
@Component({
props: {
propMessage: {
type: String,
default: ""
}
},
watch: {
"message": [
{
handler: "onMessageChanged",
immediate: true,
deep: true
}
]
},
methods: {
onMessageChanged(newMessage, oldMessage): string { /*処理*/ }
}
})
@Prop({ default: "" }) propMessage!: string
@Watch("Message", { immediate: true, deep: true })
onMessageChanged(newMessage: string, oldMessage: string): string { /*処理*/ }
これでpropsとwatchがかなり簡潔に書けました。20行のコードが3行になりました。
まとめ
最後に、改めてJavaScriptとTypeScriptのコード全体を比較してみましょう。
まずJavaScript。
module.exports = {
// 型と初期値を持つprops
props: {
propMessage: {
type: String,
default: ""
}
}
data: function() {
return {
message: 123 // 初期データ
}
}
// 算出プロパティ
computed: {
computedMessage: function() {
return 'computed ' + this.message;
}
}
// 監視プロパティ
watch: {
message: {
handler: function(newMessage, oldMessage) { /*処理*/ },
immediate: true,
deep: true
}
}
// ライフサイクルフック
mounted: function() {
this.greet();
}
// 普通のメソッド
methods: {
greet: function() {
alert('greeting: ' + this.message);
}
}
}
次にTypeScript。
import Vue from 'vue'
import { Component, Vue, Prop, Watch } from "vue-property-decorator";
@Component
export default class App extends Vue {
@Prop({ default: "" }) propMessage!: string
@Watch("Message", { immediate: true, deep: true })
onMessageChanged(newMessage: string, oldMessage: string): string { /*処理*/ }
// 初期データをクラスのプロパティとして記述できる
message: string = 123;
// 算出プロパティは読み取り専用プロパティとして記述できる
get computedMessage(): string => 'computed ' + this.message;
// ライフサイクルフックをクラスメソッドとして記述できる
mounted(): void => this.greet();
// 普通のメソッドもクラスメソッドとして記述できる
greet(): void => alert('greeting: ' + this.message);
}
比較しても分かる通りTypeScriptではコードを簡潔に、しかも型安全に書けます。
Vue+TypeScriptのススメ、お分かりいただけただろうか?
おまけ
フォームの内容を型安全にやりとりする方法
フォームを実装する際、簡単なStoreパターンを用いて内容を外部とやりとりしたいと思いました。その際、フォームの内容を型安全にやり取りしたいと思って試行錯誤した結果、以下のやり方に落ち着いたので紹介しておきます。
やり方はフォームの内容を表すクラスを定義し、フォームの初期値をプロパティの初期値として定義することです。こうすることでそのクラスのインスタンスを渡すことでフォームに初期値が設定できます。
import { Component, Vue, Prop } from "vue-property-decorator";
export class InqueryFormFields {
platform: string = "psn";
id: string = "";
}
@Component
export default class InqueryForm extends Vue {
@Prop(InqueryFormFields) formInitialValues!: InqueryFormFields
// formInitialValuesが存在する場合はformへ値渡しする
form = this.formInitialValues ? Object.assign(new InqueryFormFields(), this.formInitialValues) : new InqueryFormFields();
onSubmit(): void => this.$emit("submit", Object.assign(new InqueryFormFields(), this.form));
}
(殆どのフォームがそうかと思いますが)上記の例ではInqueryFormFieldsインスタンス
の参照を渡すと意図しない挙動になるので、Object.assign()
でInqueryFormFieldsインスタンス
の値をコピーしています。
外部から渡されたインスタンスの値をコピーするのはInqueryForm
の責務なので、InqueryForm
内に実装を隠蔽する形にしています。こうすることで外部のクラスがわざわざ値をコピーする必要がなくなります。