LoginSignup
1
1

More than 3 years have passed since last update.

VueでWatch対象のパスを安全に指定できるライブラリ vuekey を作った

Posted at

作ったもの

Vueで値を監視して変更があったらハンドラを発火させるWatchですが、パスの指定が文字列なので存在しないパスを指定しても誰も怒ってくれません。

そこで、vue-property-decorator@Watchデコレータ向けにデータへのパスを安全に指定できるライブラリvuekeyを作りました。

@lollipop-onl/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でラップするという手段で実装を行っています。

該当箇所

もっと良い方法があればコメントで教えていただきたいです!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1