作ったもの
Vueで値を監視して変更があったらハンドラを発火させるWatch
ですが、パスの指定が文字列なので存在しないパスを指定しても誰も怒ってくれません。
そこで、vue-property-decorator
の@Watch
デコレータ向けにデータへのパスを安全に指定できるライブラリvuekey
を作りました。
この記事では、簡単に使い方と内部実装についてご紹介します。
使い方
インストール
まずは、@lollipop-onl/vuekey
をインストールしてください。
$ npm i --save @lollipop-onl/vuekey
# or
$ yarn add @lollipop-onl/vuekey
Vueファイル内でセットアップ
vuekey
ではsetupVuekey
という関数を提供しています。
この関数のGenericとしてVueクラスを渡すと、そのクラス内で有効なパラメータへのパスを検証することができます。
import { Component, Vue } from 'vue-property-decorator';
import { setupVuekey } from '@lollipop-onl/vuekey';
// vuekeyのセットアップ
const vuekey = setupVuekey<SampleComponent>();
@Component
export default class SampleComponent extends Vue {}
vuekey
でパスを指定
vuekey(<key>, <key>, ...)
とパラメータのキーを指定していきます。最大10個ネストしたパラメータまでを検証できます。
import { Component, Watch, Vue } from 'vue-property-decorator';
import { setupVuekey } from '@lollipop-onl/vuekey';
// vuekeyのセットアップ
const vuekey = setupVuekey<SampleComponent>();
@Component
export default class SampleComponent extends Vue {
profile: {
name: string;
age?: number;
works: IWark[];
updatedAt: number;
};
// プロフィールが変更されたらタイムスタンプを更新する
@Watch(vuekey('profile', 'name'), { immediate: true })
@Watch(vuekey('profile', 'age'))
onChangeProfile() {
this.$set(this.profile, 'updatedAt', Date.now());
}
}
これで、存在しないパラメータキーを指定しようとしたらエラーとなります。
vm.$set
もタイプセーフにする
this.$set
はオブジェクトや配列の要素をリアクティブに変更するためのビルトインメソッドですが、第2引数、第3引数は方に縛られていません。
ここも存在するキー、代入可能な型かどうかを検証させる機能をvuekey
に持たせています。
先程のプロフィールのタイムスタンプを更新する例は以下のように変更できます。
import { Component, Watch, Vue } from 'vue-property-decorator';
import { setupVuekey } from '@lollipop-onl/vuekey';
// vuekeyのセットアップ
const vuekey = setupVuekey<SampleComponent>();
@Component
export default class SampleComponent extends Vue {
profile: {
name: string;
age?: number;
works: IWark[];
updatedAt: number;
};
// プロフィールが変更されたらタイムスタンプを更新する
@Watch(vuekey('profile', 'name'), { immediate: true })
@Watch(vuekey('profile', 'age'))
onChangeProfile() {
this.$set(...vuekey(this.profile, 'updatedAt', Date.now()));
}
}
これで、this.profile
の型定義に存在しないプロパティの指定や、不正な型の代入を防ぐことができます。
Vueで文字列だからとおざなりになりがちなところですが、型安全にして平和にいきましょう!
実装のこと
ここからは、vuekey
を作るにあたって困った点を備忘録として残します。
同じ轍を踏む人が一人でも減りますように...
前の引数で指定したプロパティの持つキーのみを続く引数で指定させる
vuekey
では、第1引数で指定したパラメータのキーを第2引数の型とするような機能が必要でした。
当初は
// vuekey<V extends Vue>は省略しています
function vuekey<V>(t1?: keyof V, t2?: keyof V[typeof t1], t3: keyof V[typeof t1][typeof t2], ...)
のように、前の引数の型を後半でも参照するようにしていました。
しかし、この実装では前半の型は確定しなかったため没になりました。
function vuekey<V>(t1?: keyof V, t2?: keyof V[typeof t1] // typeof t1が確定していないのでV[typeof t1]がneverになった
そこで、インターフェースのジェネリックで内部状態(?)として型を指定するようにしました。
interface Vuekey<V extends Vue> {
<
T1 extends keyof V,
T2 extends keyof V[T1],
T3 extends keyof V[T1][T2]
>(t1?: T1, t2?: T2, t3?: T3): string;
}
こうすることで、T2
でのT1
は実際に指定された定数値を参照することができました。
Nullableなオブジェクトでのkeyof
任意のクラスメンバーは | undefined
となりますが、このような値に対してkeyof
するとnever
が返されてしまいます。
任意のパラメータを除去するにはRequired
というビルトインの型を使用します。
さて、任意のパラメータがネストする場合はどうすればよいでしょうか?おそらくDeepRequired
みたいな型を作れば良いと思います。
type DeepRequired<T> = T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T;
ただし、vuekey
の場合は事情が違います。vuekey
では、T
にあたる型の親のパラメータが任意である可能性があり、DeepRequired
は利用できませんでした。
そこですべての型をRequired
でラップするという手段で実装を行っています。
もっと良い方法があればコメントで教えていただきたいです!