ジェネリックコンポーネント
前編で紹介した新機能に続いて、次に紹介するのは、新機能リストの5です。これは、scriptタグに新たに導入されたgeneric属性についてです。具体的なコードを見る前に、少しサンプルを紹介したいと思います。
各組の人数を表示するサンプル
例えば、図1のような画面を考えてみます。
これは1〜6組の各クラスの人数とA〜F組の各クラスの人数を表示した画面です。この画面の元データとして考えられるのがリスト1の(1)と(3)の2種のMapオブジェクトです。
<script setup lang="ts"> : const classSizeNoInit = new Map<number, number>(); classSizeNoInit.set(1, 35); classSizeNoInit.set(2, 34); : const classSizeNo = ref(classSizeNoInit); // (1) const focusedClassNo = ref(4); // (2) const classSizeCharInit = new Map<string, number>(); classSizeCharInit.set("A", 35); classSizeCharInit.set("B", 34); : const classSizeChar = ref(classSizeCharInit); // (3) const focusedClassChar = ref("B"); // (4) </script> <template> <section> <h2>1〜6組の人数</h2> <ClassSizeList v-bind:classSizeList="classSizeNo" v-bind:focused="focusedClassNo"/> // (5) </section> <section> <h2>A〜F組の人数</h2> <ClassSizeList v-bind:classSizeList="classSizeChar" v-bind:focused="focusedClassChar"/> // (6) </section> </template>
リスト1の(1)は、組の識別子が1組、2組、…、のような数字になっています。一方で、(3)は、A組、B組、…、のように文字になっています。それに合わせて、Mapのキーのデータ型は、(1)の元データとなるclassSizeNoInitではnumber型を、(3)の元データであるclassSizeCharInitではstring型としています。同様に、図1で「注目!!!」と表示させる組を表すリアクティブ変数である(2)と(4)に関しても、(2)はnumber型であり(4)はstring型となっています。
そして、これらのデータを子コンポーネントであるClassSizeListにPropsで渡して、図1のようなレンダリングにしたいとします。それが、(5)と(6)です。Prop名は、それぞれclassSizeListとfocusedです。
ユニオン型Props定義の問題点
では、このClassSizeListコンポーネントのProps定義はどのようにすればよいのかというと、まず思いつくのはユニオン型の定義です。これは、リスト2のようになります。
type Props = { classSizeList: Map<number | string, number>; focused: number | string; };
これでとりあえずは動作します。ただし、問題があります。それは、例えば、リスト1の(5)において、v-bind:focusedとして渡さなければならない値、すなわち数値型であるはずのfocusedClassNoに「C」のような文字を渡しても、問題なく動作してしまうということです。コード上もエラーになりませんし、レンダリング結果でもエラーになりません。ただ、「注目!!!」の表記がなくなるだけです。この種類のバグは非常に発見しにくいです。
generic属性によるジェネリックコンポーネント
そこでVue3.3で新たに導入されたのが、ジェネリックコンポーネント、すなわち、scriptタグのgeneric属性です。これを利用したClassSizeListコンポーネントは、リスト3のコードとなります。
<script setup lang="ts" generic="CN"> // (1) type Props = { classSizeList: Map<CN, number>; // (2) focused: CN; // (3) }; defineProps<Props>(); </script>
リスト3の(1)では、scriptタグにgeneric属性としてCNを定義しています。この記述でいわゆるジェネリクスとして機能するようになります。もちろん、ジェネリクスですので、CNという記述は、なんでもよく、TでもTypeでもかまいません。
そして、このgeneric属性で指定した文字列をコンポーネント内では型指定として利用できます。(2)ではPropsのclassSizeListであるMapオブジェクトのキーとしてCN型を指定しています。同様に、Propsのfocusedも同じCN型となっています。こうすることで、classSizeListであるMapオブジェクトのキーのデータ型とfocusedのデータ型が同一でなければならなくなります。
そして、このCN型がどのようなデータ型になるかは、このコンポーネントを利用する側から渡されるPropsのデータ型から自動判定されるようになっています。例えば、リスト1の(5)では、Mapのキーとしてnumber型を指定したclassSizeNoをPropsのclassSizeListとして渡しています。この時点でCNがnumberと判定され、Propsのfocusedもnumber型である必要がでてきます。仮に、これに反して文字列を渡すとすると、図2のようにコーディング段階でエラー表示してくれます。
もちろんリスト1の(6)のCNがstringとなるパターンにおいても、focusedにnumber型の値を渡すと、同様にエラーとなります。実行してもエラーとならずにバグとなるユニオン型でのProps定義と比べると、コーディング段階からエラー表示となるこのジェネリクスコンポーネントとでは、安心感に雲泥の差がありますね。