前提事項
- Vue.js 3.3以降(リリース情報:May 11, 2023 Announcing Vue 3.3)
- TypeScriptの設定ファイル、tsconfig.jsonで
strict: true
が設定されていること - Vue.jsでコンポーネントを実装する方法や、Propsについて理解できていること
tsconfig.jsonはextendsで拡張できるので、プロジェクトルートのtsconfig.jsonには記載されていない場合でも、extendsしている元のtsconfig.jsonでstrict: true
が設定されている場合があります。
任意のデータを表示するコンポーネントの型問題
例として、チェック機能をもつテーブルコンポーネントを実装したいとします。
Element PlusにあるようなこのようなUIですね。
- 任意のデータ配列を表示できる
- チェックしたレコードを配列として取得できる
- チェック操作した列を個別に取得できる
「任意のデータが渡せる」ということは、コンポーネント側でPropsを固定の型にできません。そのためunknown[]
といったような任意の値を含む配列型を設定していました。
unknown
はany
と違って型を放棄しません。
任意のデータを受け取りつつ、そのデータを利用する際に型を絞り込んでから利用します。
サンプルコードと問題となる箇所
下記の仕様を実装したサンプルコードです。
- 任意のデータ配列を表示できる
- チェックしたレコードを配列をして取得できる
- チェックを操作した列を個別に取得できる
// NoGenericTab.vue
<script lang="ts" setup>
// props
// ------------------------------
type Props = {
items: unknown[];
};
const { items } = defineProps<Props>();
// v-model
// ------------------------------
const selectedItems = defineModel<typeof items>('selectedItems', { default: [] });
// カスタムイベント
// ------------------------------
const emit = defineEmits<{
check: [value: (typeof items)[number]];
}>();
const handleCheck = (value: (typeof items)[number]) => {
emit('check', value);
};
</script>
<template>
<!-- title -->
<h2>NoGenericTable</h2>
<!-- table -->
<table class="custom-table">
<tr v-for="item in items">
<td><input type="checkbox" v-model="selectedItems" :value="item" @change="handleCheck(item)" /></td>
<!-- loop td -->
<td v-for="value in item">{{ value }}</td>
</tr>
</table>
</template>
これを利用するときは以下のようなイメージです。
<script lang="ts" setup>
import { ref } from 'vue';
import NoGenericTable from './NoGenericTable.vue';
import { SampleData, sampleData } from './sampleData'
// for NoGenericTable
// ------------------------------
const selectedItems = ref<SampleData[]>([]);
const selectedItem = ref<SampleData>();
const handleCheck = (value: SampleData) => {
selectedItem.value = value;
};
</script>
<template>
<NoGenericTable
:items="sampleData" // ★ :itemsの型はSampleData[]にはならずunknown[]となる
@check="handleCheck" // ★ ここで型エラーになる
v-model:selected-items="selectedItems"
></NoGenericTable>
<p>チェック操作したアイテム:{{ selectedItem }}</p>
</template>
tsconfig.jsonのstrict: true
が設定されている場合、この時の:items
の型はSampleData[]
にはならずunknown[]
となります。
また @check="handleCheck"
でコンポーネント側からチェック操作した要素を受け取っていますが、この要素の型もunknown
となるため、handleCheck
の引数の型(value: SampleData)
にunknown
をセットできないというエラーが表示されます。
データ | 期待する型 | 実際の型 |
---|---|---|
:items | SampleData[] | unknown[] |
handleCheckの引数に渡されるデータ | SampleData | unknown |
このエラーを解決するには、型を放棄してanyとするか、型ごとにコンポーネントを実装するか、型ガードを実装するかの選択になり、実装に手間がかかります。
Vue 3.3からはこのような型の問題を解決する方法として、generic="T"
が提供されました。
ここからはまずTypeScriptのジェネリクス型について紹介し、generic="T"
の使い方をサンプルコードとともに紹介していきます。
ジェネリクス型とは
TypeScriptのジェネリクス型についてまず説明します。
以下のサンプルコードはサバイバルTypeScript ジェネリクス (generics)より引用させていただきます。
処理は同じだが、型が違う
たまにそういうことありますよね。
そういう場合、型を犠牲にして処理を共通化するか(引数をanyにするなど)、処理を分けて型安全を担保するか、という2択を迫られます。
下記は処理を分けて型安全を担保しています。
// 処理が重複した関数
// ------------------------------
/** 文字列用の関数 */
function chooseRandomlyString(v1: string, v2: string): string {
return Math.random() <= 0.5 ? v1 : v2;
}
/** 数値用の関数 */
function chooseRandomlyNumber(v1: number, v2: number): number {
return Math.random() <= 0.5 ? v1 : v2;
}
// 文字列用の関数はstrngにしか対応していない
const resultString = chooseRandomlyString('a', 'b');
// 文字列用の関数に数値を渡すを型エラーになる
// const resultNumber = chooseRandomlyString(0, 1);
const resultNumber = chooseRandomlyNumber(0, 1);
そういった問題をジェネリクス型は解決します。
<T>
のところが型引数と呼ばれる機能で、型情報を引数として渡すことができます(T
は任意の文字でよく、型推論が利く場合は指定しなくてもよい)。
// Generic型でコードの共通化と型安全を担保した関数
// ------------------------------
/** 任意の引数を受け取る関数 */
function chooseRandomly<T>(v1: T, v2: T): T {
return Math.random() <= 0.5 ? v1 : v2;
}
// 任意の型を渡すことができる
const resultString2 = chooseRandomly('a', 'b');
const resultNumber2 = chooseRandomly(0, 1);
以上を踏まえて、Vue.jsで実装されたコンポーネントに、これと同じ機能をもつジェネリクス型をPropsに設定する方法について解説します。
generic="T" で型情報も受け取る
上記の問題をTypeScriptの関数同様にジェネリクス型で解決します。
以下のようにして設定します(Tは任意、個数も任意)。
<script setup lang="ts" generic="T">
<script setup lang="ts" generic="T extends { id: number }">
公式の<script setup>
ジェネリクスのページにも載っていますが下のほうにあって気が付きませんでした、、Vue3.3で利用できるようになっていたなんて!(リリースは2023/11で執筆時点から2年くらいも前、、!)
サンプルを実装してみる
上記のVueのプレイグラウンドにアクセスすると下記のような画面が表示されると思います。
GenericTable
はジェネリクス型で実装されているため、:items
のデータ型から@check
で渡されるデータ型を推測できており型エラーが発生しません。
いっぽう、NonGenericTable
のほうは@check
で渡されるデータがunknown
型になっているため型エラーとなります。
まとめ
- 任意のデータを利用する処理では
- 型を犠牲にして処理を共通化するか(引数をanyにするなど)
- 処理を分けて型安全を担保するか
という問題があった
- TypeScriptにはそれを解決できるジェネリクス型がある
- Vue3.3以降でコンポーネントのPropsにジェネリクス型を設定できるようになった