VisasQ Dev Blog

ビザスク開発ブログ

@vue/composition-api のバージョンアップを型検査で乗り切った話

アドバイザー/lite開発チーム フロントエンドエンジニアの小柳(@mascii_k)です。

はじめに

弊社サービス「ビザスクlite」では、Vue 2.6 向けのプラグイン @vue/composition-api を 2020/02 から導入しています。

tech.visasq.com

ビザスクliteでの導入実績とノウハウ蓄積の結果、社内で利用している管理画面のフロントエンド(Vue 2.6)環境にも @vue/composition-api を導入することになりましたが、当時の判断で古いバージョンのものを導入してしまいました。

しくじりの経緯

2020/09 当時の最新バージョンであった @vue/composition-api 1.0.0-beta.14 を導入しようとしたところ、vue 本体のバージョンが 2.6.10 (当時の最新バージョンは 2.6.12)だったため、コンパイル時に型に関するエラーが発生することがわかりました。

そのため、vue 本体のバージョンを 2.6.12 に予めバージョンアップしておくことを考えましたが、当時は組織内において vue のバージョンアップに対する不安感がありました。

少し古いバージョンの @vue/composition-api 0.6.6 であれば vue のバージョンが 2.6.10 のまま導入できることがわかり、これを導入することにしました。

古いバージョンの導入後に判明した問題

次第に 0.6.6 では import { nextTick } from '@vue/composition-api'nextTick() を import できなかったり、最新バージョンとの間に多くの機能差や破壊的変更があることがわかりました。

特に 1.0.0-beta.7 での破壊的変更 "template auto ref unwrapping are now applied shallowly" によって、ref()computed() などが返す Ref のアンラップの挙動が変更となったことはバージョンアップ時の懸念点となっていました。実際、この破壊的変更の前でしか動作しないコードをレビューで見たことがありました。

以下は "template auto ref unwrapping are now applied shallowly" の影響を受けるコードと動作の例です:

<template>
  <div>
    <h2>@vue/composition-api@0.6.6</h2>
    <div>
      {{ count }} * 2 = {{ multiples.double }}
    </div>
    <div>
      {{ count }} * 3 = {{ multiples.triple }}
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, computed } from '@vue/composition-api';

export default defineComponent({
  name: 'App',
  setup() {
    const count = ref(123);

    return {
      count,
      multiples: {
        double: computed(() => count.value * 2),
        triple: computed(() => count.value * 3),
      },
    };
  },
});
</script>
0.6.6 での動作 1.4.9 での動作

バージョンアップに着手

私が "template auto ref unwrapping are now applied shallowly" の影響調査を担当することにし、vue 本体のバージョンアップと @vue/composition-api のその他の破壊的変更の影響調査は社内の別のエンジニアさんに担当していただきました。

vue-tsc を活用して型検査することを考える

1年ほど前には vue-tsc1reviewdog を用いて TypeScript の Strict Mode が有効でない環境に Strict Mode を漸進的に導入する試みを紹介しておりました。vue-tsc では .ts ファイルだけでなく、.vue ファイルの <template> ブロックと <script> ブロックも検査できます。

tech.visasq.com

今回は .vue ファイルの <template> ブロックを検査する方向ではなく、<script> ブロックを検査し "template auto ref unwrapping are now applied shallowly" の影響を受けるファイルを抽出する方向で考えました。実際に用いた手法を以下に示します:

.vue ファイルの抽出手順

  1. npm install -g vue-tsc@0.34.15 で vue-tsc をインストールする
  2. npm install @vue/composition-api@latest で最新の @vue/composition-api にバージョンアップする
  3. 検査用のコードを defineComponent を用いている .vue ファイルに埋め込むため、VSCode正規表現モードで以下の置き換えを実施する:

    置換対象(正規表現):
    export default defineComponent\(([\s\S\n]+)</script>
    置換内容:
    const __component__ = defineComponent($1
    export default __component__;
    
    import { reactive as _reactive } from '@vue/composition-api';
    const shallowUnwrapped = __component__.data!;
    const deepUnwrapped = _reactive(shallowUnwrapped);
    const typeCheck: typeof shallowUnwrapped extends typeof deepUnwrapped ? 1 : 0 = 1;
    </script>
    置換結果の例:
     <script lang="ts">
     import { defineComponent, ref, computed } from '@vue/composition-api';
    
    -export default defineComponent({
    +const __component__ = defineComponent({
       name: 'App',
       setup() {
         const count = ref(123);
    
         return {
           count,
           multiples: {
             double: computed(() => count.value * 2),
             triple: computed(() => count.value * 3),
           },
         };
       },
     });
    +
    +export default __component__;
    +
    +import { reactive as _reactive } from '@vue/composition-api';
    +const shallowUnwrapped = __component__.data!;
    +const deepUnwrapped = _reactive(shallowUnwrapped);
    +const typeCheck: typeof shallowUnwrapped extends typeof deepUnwrapped ? 1 : 0 = 1;
     </script>
    
  4. vue-tsc で型検査を実施する
    vue-tsc --noEmit --allowJs | grep "TS2322: Type '1' is not assignable to type '0'."

なぜこれで抽出できるか?

1.0.0-beta.7 以降では __component__.data!setup 関数の戻り値を shallow unwrap ref した型を持っていて、さらに reactive() に通すことで deep unwrap ref した型を得ることができます2
Conditional Types の extends で 2 つの型の構造を比較し、構造が異なる場合は const 宣言の型エラーを意図的に発生させることで "template auto ref unwrapping are now applied shallowly" の影響を受けるファイルを抽出できます。

抽出の結果

コマンド vue-tsc --noEmit --allowJs | grep "TS2322: Type '1' is not assignable to type '0'." の結果は以下のようになりました:

src/modules/*****-******/organisms/****************.vue(47,7): error TS2322: Type '1' is not assignable to type '0'.
src/modules/*****-******/organisms/***************.vue(54,7): error TS2322: Type '1' is not assignable to type '0'.
src/modules/*****-******/templates/*****************.vue(102,7): error TS2322: Type '1' is not assignable to type '0'.
src/modules/*****-******/templates/***********************.vue(113,7): error TS2322: Type '1' is not assignable to type '0'.
src/modules/*******/organisms/*****************.vue(150,7): error TS2322: Type '1' is not assignable to type '0'.
src/modules/*******/organisms/******************.vue(144,7): error TS2322: Type '1' is not assignable to type '0'.
src/modules/*******/organisms/*****************.vue(153,7): error TS2322: Type '1' is not assignable to type '0'.
src/modules/*****-******/organisms/*************.vue(220,7): error TS2322: Type '1' is not assignable to type '0'.
src/modules/*******/organisms/**************************.vue(273,7): error TS2322: Type '1' is not assignable to type '0'.
src/modules/*******/pages/*************.vue(309,7): error TS2322: Type '1' is not assignable to type '0'.
src/modules/*******/organisms/********************.vue(311,7): error TS2322: Type '1' is not assignable to type '0'.

@vue/composition-api に依存している .vue ファイルは約 200 ファイルありましたが、11 ファイルまで絞り込みができました。

上記 11 個の .vue ファイルの <template> ブロック上で "template auto ref unwrapping are now applied shallowly" の影響を受けている箇所を目視確認し、漏れなく確実に修正できました。

まとめ

今回のような検査が必要となったのは vue 本体のバージョンアップの機運があったにもかかわらず、バージョンアップしなかったことが起因しています。
当時からバージョンアップのリスク評価を正しくできていれば、組織内の不安感も解消できていたはずですので、今回の反省を活かしていきたいと思っています。


  1. vue-tsc@0.3.0 よりも上のバージョンだと弊社の環境においては型検査できませんでした vue-tsc@0.34.15 (2022/05/16 時点での最新版)で型検査が可能となっていました

  2. reactive は、ref のリアクティビティを維持しながら、全ての深さの ref をアンラップします - https://v3.ja.vuejs.org/api/basic-reactivity.html#reactive