---
url: /blog-vue/vue-generic-components.md
description: Vue 3.3 終於新增了泛型元件（Generic Component），可以在 TypeScript 環境中得到更準確的型別提示了！ˋ( ° ▽、° )
---

![vue-generic-components.jpg](/vue-generic-components.webp){.cover}

# Vue 泛型元件，讓 TypeScript 更精確

大家好，我是鱈魚。%（＾∀＾●）ﾉｼ%

Vue 3.3 終於新增了泛型元件（Generic Component），可以在 TypeScript 環境中得到更準確的型別提示了！%ˋ( ° ▽、° )%

這個泛型元件與 TypeScript 的 Generic 是同一個概念，不知道 Generic 的朋友可以先來來[複習一下](https://www.typescriptlang.org/docs/handbook/2/generics.html)。

甚麼？你說你沒有使用 TypeScript？抱歉浪費您 5 秒鐘，可以上一頁了。%（＞人＜；）%

或者在離開前路過[文件](https://vuejs.org/guide/typescript/overview#generic-components)湊個熱鬧。%( •̀ ω •́ )y%

有使用 TypeScript 的朋友就讓我們一起繼續看下去吧。%ლ(╹◡╹ლ)%

## 來個 select

假設我需要一個綁定物件的 select，最常見的問題就是 `modelValue` 只能 `any`，如下：

`SelectAny.vue`

```vue
<script setup lang="ts">
// ...（省略其他程式碼）

interface Prop {
  modelValue: any;
  label?: string;
  options: any[];
  optionLabel?: (option: any) => string;
  /** 是否多選 */
  multiple?: boolean;
}

// ...
</script>
...
```

### v-model 這個糖

忘記元件怎麼實現 v-model 的朋友們別擔心，讓我們複習一下。

::: tip
Vue 3.4 開始可以使用 `defineModel`，不過與此文主題無關，所以我們先使用經典寫法。
:::

v-model 只是一個語法糖，實際上的程式碼如下（[文件](https://vuejs.org/guide/components/v-model.html#under-the-hood)）。

```vue
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  >
</template>
```

`modelValue` 就是用來表示預設綁定值的變數，讓我們回到剛剛的 `any`。

所以現在就算我們的 `modelValue` 與 `options` 的型別不同，或者  `multiple` 為 `true`，而 `modelValue` 卻沒有給矩陣時，都不會有任何錯誤提示。

```vue
<script setup lang="ts">
// ...
import SelectAny from './SelectAny.vue'

const options: User[] = [
  { name: '鱈魚', price: 10000 },
  { name: '章魚', price: 100 }
]

const anyDatum = ref<{ age: number }>()
const selectedAnyDatum = ref<User>()
</script>

<template>
  <!-- anyDatum 與 option 型別不同 -->
  <select-any v-model="anyDatum" :options="options" :option-label="getLabel" />

  <!-- modelValue 沒有給矩陣 -->
  <select-any v-model="selectedAnyDatum" multiple :options="options" :option-label="getLabel" />
</template>
```

畢竟是 any 嘛。%(˘･\_･˘)%

## 加上泛型

如果加上元件泛型就不會有以上問題了。

`SelectGeneric.vue`

```vue
<script
  setup
  lang="ts"
  generic="ModelValue, Multiple extends boolean"
>
// ...

interface Prop {
  modelValue: Multiple extends true ? ModelValue[] : ModelValue;
  label?: string;
  options: ModelValue[];
  optionLabel?: (option: any) => string;
  /** 是否多選 */
  multiple?: Multiple;
}

// ...
</script>
// ...
```

除了 `modelValue`、`options` 有泛型 `ModelValue` 外，我們還加上 `multiple` 判斷，如果為 true，則 `modelValue` 為 `ModelValue`\[]，否則為 `ModelValue`。

這樣子就可以達成以下效果：

* `modelValue` 與 `options` 型別不同時警告，
* `multiple` 為 true 時，如果 `modelValue` 沒有提供矩陣時發出警告。

```vue
<script setup lang="ts">
// ...
import SelectGeneric from './SelectGeneric.vue'

const options: User[] = [
  { name: '鱈魚', price: 10000 },
  { name: '章魚', price: 100 }
]

const anyDatum = ref<{ age: number }>()
const selectedGenericDatum = ref<User>()
const selectedGenericData = ref<User[]>([])
</script>

<template>
  <!-- Type '{ age: number; }' is not assignable to type 'User | User[]'.ts(2322) -->
  <select-generic v-model="anyDatum" :options="options" :option-label="getLabel" />

  <!-- Type 'User' is not assignable to type 'User[]'.ts(2322) -->
  <select-generic v-model="selectedGenericDatum" multiple :options="options" :option-label="getLabel" />

  <!-- 正確 -->
  <select-generic v-model="selectedGenericData" multiple :options="options" :option-label="getLabel" />
</template>
```

TypeScript 成功重返光榮。%o((>ω< ))o%

文章到此結束了，感謝您的閱讀，以上程式碼可以[在此](https://play.vuejs.org/#eNrtWM1u20YQfpUxL6INRWrQnlRJRpoYRYLmB3Xai+gDLS1lptSS4C5lGwIPvcVAgBQ9FUEPPaQ9tCiM9lSg6dvEbn3KK3R2lrsiKdlJHOWQID4Y5OzMzjezs9+MOHOuJUlrmjGn43TFMA0TCYLJLIHI5+Oe50jhOX2Ph5MkTiXMIGUB5BCk8QQaaNb4tLT2lWCpXWy15WHChFKwKtssYkN5jR9aHStRGEqbafnnjLM0HNa0C6mx8Pgw5kJCnMgQHzoEY7ADPRh4HBAW9yesA42zPx6e/f6k0YQEjVFw9SP8g7xZUfr3t5/qSpB7fIf8BBkfKh8wZvILf5dFrvapXa7DTG2VYvZSXqBpqW3RFrcwMH1+eMOX2QTxYS67M/DH6Idnk12VvL67jupaU2C0ko0wOWUD5Upr1fSKtCzXPUfTLykOdvruYEcpd9u6EvDc8UWySRL5kuEbQHfvat+eWbeNbySdH+z0yiQesQgrpwbfc6BTHBEuFk9z4ZVIJRRXTG5xqU0Aak7h7NHxf78+hbtk9uLZw9M/vz/98QjubxcrL54dzYEl/dNHR8//eQzXZRrByePj53//fPL0ydm3f3XbyfnYzSGtDPTtLJJhErG3hHcx15PC4eXxJ/3ZbKEG81zj0BGmC7EWpVWvDHORFxCXi/ZNkm2KFD2EAeoEfiQYMRfg3wK+xSI670QuCGEFRbIMWrlULoFqeWLfqBzQW7vEAgvVUWGeV6wQG+Ylav8VgvZXEPPyEH0bYSUpThNbJbJsEI5bD0TMsZ9SO/CcYTxJwoilutbQdUc3CrXmR1G8f4tkMs0Y9SKy2WPDb5bIH4gDJfOceylDyp5iids16acYg17e2r7DDvDZLmKisgi1L1j8kok4yhRGrfZZxkcIu6RHaG9Sgw75+L7YOpCMCxOUAqo0dUf1HGzP1y8IfQ7349YnZIdtErNYmQheYyppgoy/9iNMFuz7crjXBJX4DM+uPq+gHZcsDfwhg3tpnGhMVEq0QUf1aNQD9IYFsdkBIVMMmER2ykCdgRoLjIiKB3XtTIAK69Drl43bGxtw+sPxyXe/6LqGjTa5LgoVrXfjOGI+1yODbtqJgtiD/VDu3WCBj7rCHbEg5EyBF131H5t8U4dRAmOxEIxb23fvtDSWMDg0K3RWxn8H8Mhp5xHK8/X5iMEmoUQMenELX0SXvDWyZIT135knr9GBwVRnUQEbNEpLOypfxYhTnaZu8lCSjovb+UXuyEMYgLumhOtmslIvlE0NDS8CwkdwFGDip4K5tVjJXDvVG6qUtkzUhSM7uRUbbpoHDEifMw1xNbXqbEeB65HKrUSlHdpMEBiqUpdMmuDSGREQlWt3SWJtgWsb2iPHU8dTYYm+fpCX50LLfz17FdySH5UINaLHAZWYzQf0ej1o6Ow16slRXpYkY22tsoVC9rJBchROTSuja2Y6N73g7Ubu1XJFt/Q075mKlIeYbsv/lBFF6gYBytbWzIudA9BU1z2aBnFq+wGEZmCnzkC72cWSNf5QmJVvmL1fOaajcNDWMhNbW2MtuqgOut44Fn7WlGhP2Wnq04Rk6Y/ex9oGRbdtoTRtawWkecZHwrCK53gK19tizQW3VJKbMIeGv8pQzb6+lGPLliunWgN3tVxbBv2e8G6VAapcsYnkiGdqOZnuwXtKzLWwqmwMvliSeIJ3PqHrvT4Q+cVE/q7QOH3vakmB7M0OiGLnjEmfxmZequz1pybDVSQqvjbpr0Aky538f3lI90M=)取得。

有其他想法請不吝告訴我，鱈魚感謝您！%(o゜▽゜)o☆%

## 總結 🐟

* Vue 3.3 新增泛型元件
* 泛型元件可以讓 TypeScript 推導更精確
