深入了解 TypeScript 後,讓我們將其與 Vue Composition API 結合使用。有助於我們構建更具可維護性和型別安全性的 Vue 元件,降低錯誤風險,提高開發效率。透過範例,我們將更好的了解如何在 Vue3 中運用 TypeScript 和 Composition API。那麼,我們開始吧!
import { ref } from 'vue';
const name= ref('肉鬆'); // 推斷出的型別:Ref<string>
name.value = 2023 // TypeScript 報錯,Type 'number' is not assignable to type 'string'
Ref
型別ref
內的值指定一個更複雜的型別,可以使用 Ref
型別:
import { ref } from 'vue';
import type { Ref } from 'vue';
const name: Ref<string | number> = ref('肉鬆');
name.value = 2023; // 通過
import { ref } from 'vue';
const name = ref<string | number>('肉鬆');
name.value = 2023; // 通過
當我們泛型參數未給初始值,會得到包含 undefined
的聯合型別:
import { ref } from 'vue';
const name = ref<string | number>(); // 推斷得到的型別:Ref<number | undefined>
import { reactive } from 'vue';
const theme = reactive({ title: 'TypeScript' }) // 推斷得到的型別:{ title: string }
reactive
變數型別import { reactive } from 'vue';
interface Theme {
title: string;
}
const theme: Theme = reactive({ title: 'TypeScript' });
需要注意的是,不推薦使用 reactive()
的泛型參數,因為處理深層 ref
解包的返回值與泛型參數的型別可能不同。
import { reactive } from 'vue';
interface Person {
age: ref<number>;
name: string;
}
// 不推薦 reactive 使用泛型參數
const person = reactive<Person>({
age: ref(30), // 此時 age 型別不是 number,而是 Ref<number>
name: '肉鬆',
});
<script setup>
當我們使用 <script setup>
時,defineProps()
宏函式(Macro Functions)支援參數中推斷型別,意味著可以自動推斷 props
的型別:
<script setup lang="ts">
const props = defineProps({
name: {
type: String,
required: true,
},
age: Number,
});
props.name; // string
props.age; // number | undefined
</script>
這被稱之為「執行時宣告」,因為傳遞給 defineProps()
的參數會作為執行時的 props
選項使用。
然而,通過泛型參數來定義 props
的型別通常更直接:
<script setup lang="ts">
const props = defineProps<{
name: string;
age?: number;
}>();
</script>
這被稱之為「基於型別的宣告」。編譯器會盡可能的嘗試根據型別參數推斷出等價的執行時選項。在這種場景下,我們第二個例子中編譯出的執行時選項和第一個是完全一致的。基於型別的宣告或者執行時宣告可以擇一使用,但是不能同時使用。
我們也可以將 props
的型別移入一個單獨的介面中:
<script setup lang="ts">
interface Props {
name: string;
age?: number;
}
const props = defineProps<Props>();
</script>
為了執行正確的程式碼,傳給 defineProps()
的泛型參數必須是以下之一:
defineProps<{ /*... */ }>();
interface Props {/* ... */}
defineProps<Props>();
介面或物件字面值型別可以包含從其他文件導入的型別引用,但是,傳遞給 defineProps
的泛型參數本身不能是一個導入的型別:
import { Props } from './other-file'
defineProps<Props>(); // 不支援
這是因為 Vue 元件是單獨編譯的,編譯器目前不會抓取導入的文件以分析源型別。
當使用基於型別的宣告時,我們失去了為 props
宣告默認值的能力。這可以通過 withDefaults
編譯器宏(Compiler Macros)解決:
export interface Props {
name?: string;
hobbies?: string[];
}
const props = withDefaults(defineProps<Props>(), {
name: '肉鬆',
hobbies: () => ['運動', '看書'],
});
這將被編譯為等效的運行時 props default
選項。此外,withDefaults
幫助程式為默認值提供型別檢查,並確保返回的props
型別刪除了已宣告默認值的屬性的可選標誌。
<script setup>
如果沒有使用 <script setup>
,那麼為了開啟 props
的型別推斷,必須使用 defineComponent()
。傳入 setup()
的 props
物件型別是從 props
選項中推斷而來。
import { defineComponent } from 'vue';
export default defineComponent({
props: {
name: String,
},
setup(props) {
props.name; // 型別:string
},
});
在 <script setup>
中,emit
函式的型別標記也可以通過執行時宣告或是型別宣告進行:
<script setup lang="ts">
// 執行時
const emit = defineEmits(['change', 'update']);
// 基於型別
const emit = defineEmits<{
(e: 'change', id: number): void;
(e: 'update', value: string): void;
}>();
</script>
這個型別參數應該是一個帶調用簽名的字面值型別。這個字面值型別的型別就是返回的 emit
函式的型別。我們可以看到,基於型別的宣告使我們可以對所觸發事件的型別進行更細微的控制。
若沒有使用 <script setup>
,defineComponent()
也可以根據 emits
選項自動推斷 setup
上下文中的 emit
函式的型別:
import { defineComponent } from 'vue';
export default defineComponent({
emits: ['change'],
setup(props, { emit }) {
emit('change'); // 型別檢查 / 自動完成
},
});
computed()
會自動從其計算函式的返回值上推斷出型別:
import { ref, computed } from "vue";
const count = ref(0);
// 推斷得到的型別:ComputedRef<number>
const double = computed(() => count.value * 2);
const result = double.value.split('');
// TypeScript 報錯: Property 'split' does not exist on type 'number'
使用泛型參數指定型別:
const double = computed<number>(() => {
// 若返回值不是 number 型別則會報錯
})
當我們在處理原生 DOM 事件時,應該為我們傳遞事件處理函式的參數標記型別:
<script setup lang="ts">
function handleChange(event) {
console.log(event.target.value); // event 被隱含的標記為 any 型別
}
</script>
<template>
<input type="text" @change="handleChange" />
</template>
沒有標記型別時,這個 event
參數會隱含的標記為 any
型別。這也會在 tsconfig.json
中配置了 "strict": true
或 "noImplicitAny": true
時,導致 TypesScript 報錯。因此,建議明確為事件處理函式的參數標記型別。此外,我們需要強制轉換 event
上的屬性:
<script setup lang="ts">
function handleChange(event: Event) {
console.log((event.target as HTMLInputElement).value);
}
</script>
InjectionKey
介面provide
和 inject
通常會在不同的元件之間使用。為了正確指定值的型別,Vue 提供了一個 InjectionKey
介面,它是一個泛型型別,繼承自 Symbol
,可用於確保供應者和使用者之間注入值的型別一致:
import { provide, inject } from 'vue';
import type { InjectionKey } from 'vue';
const key = Symbol() as InjectionKey<string>;
provide(key, '肉鬆'); // 若提供的是非字串值會導致錯誤
const name = inject(key); // name 型別:string | undefined
建議將注入 key
的型別定義放在一個單獨的檔案中,這樣可以讓多個元件進行引入使用。
當使用字串注入 key
時,注入值的型別是 unknown
,需要通過泛型參數明確宣告:
const name = inject<string>('肉鬆'); // 型別:string | undefined
需要注意的是,注入的值仍然可能是 undefined
,因為無法保證供應者一定會在執行時提供 provide
的值。
當提供一個默認值後,undefined
型別就可以被移除:
const name = inject<string>('肉鬆', '傑尼龜'); // 型別:string
如果我們確定該值將始終被提供,則可以強制轉換該值:
const name = inject('肉鬆') as string;
樣板引用需要透過明確指定的泛型參數以及一個初始值 null
來建立:
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const el = ref<HTMLInputElement | null>(null);
onMounted(() => {
el.value?.focus()
})
</script>
<template>
<input ref="el" />
</template>
需要注意的是,為了確保嚴格的型別安全,當訪問 el.value
時,建議使用可選串連或型別保護。這是因為在元件被掛載之前,此 ref
的值將保持初始的 null
,並且 v-if
行為而卸載參考的元素時,也可能導致其設定為 null
。
當我們需要為子元件新增一個樣板參考,以便呼叫它所公開的方法。例如,我們有一個子元件名為 MyModal,它提供了一個開啟視窗的方法:
// MyModal.vue
<script setup lang="ts">
import { ref } from 'vue';
const isOpenModal = ref(false);
const open = () => (isOpenModal.value = true);
defineExpose({
open
})
</script>
為了取得 MyModal 的型別,我們首先需要透過 typeof
取得它的型別,然後使用 TypeScript 內建的 InstanceType
工具型別來取得它的實例型別:
// App.vue
<script setup lang="ts">
import MyModal from './MyModal.vue';
const modal = ref<InstanceType<typeof MyModal> | null>(null);
const openModal = () => {
modal.value?.open();
}
</script>
總結來說,Composition API 與 TypeScript 的結合為 Vue 元件開發帶來了更好的型別安全和程式碼。透過 setup
函式、型別定義、響應式資料、計算屬性等,我們能夠更清晰的定義元件邏輯,並確保程式碼品質。自訂 Hook、資料提供和注入、模組化開發以及錯誤處理進一步提高了開發效率和可維護性。