Vue3 Composition API 完整類型標註
Composition API 與 TypeScript 結合可以獲得極佳的類型支援。本篇將詳細介紹各種類型標註方式。
一、 script setup 語法
1.1 基本結構
vue
<script setup lang="ts">
import { ref, computed } from "vue";
const count = ref(0);
const double = computed(() => count.value * 2);
function increment() {
count.value++;
}
</script>
<template>
<button @click="increment">{{ count }} x 2 = {{ double }}</button>
</template>1.2 類型推論
typescript
// ref 會自動推論類型
const count = ref(0); // Ref<number>
const name = ref('John'); // Ref<string>
const items = ref([1, 2, 3]); // Ref<number[]>二、 ref 與 reactive 類型
2.1 ref 類型標註
typescript
import { ref, type Ref } from 'vue';
// 自動推論
const count = ref(0); // Ref<number>
// 明確標註
const name = ref<string>('John');
// 複雜類型
interface User {
id: number;
name: string;
}
const user = ref<User | null>(null);
// 使用 Ref 類型
const message: Ref<string> = ref('Hello');2.2 reactive 類型標註
typescript
import { reactive } from 'vue';
// 自動推論
const state = reactive({
count: 0,
name: 'John',
});
// { count: number, name: string }
// 明確標註
interface State {
count: number;
name: string;
user: User | null;
}
const state2 = reactive<State>({
count: 0,
name: 'John',
user: null,
});2.3 shallowRef
typescript
import { shallowRef, triggerRef } from 'vue';
interface DeepObject {
nested: {
value: number;
};
}
const data = shallowRef<DeepObject>({
nested: { value: 0 },
});
// 直接修改不會觸發更新
data.value.nested.value = 1;
// 需要手動觸發
triggerRef(data);三、 computed 類型
3.1 自動推論
typescript
import { ref, computed } from 'vue';
const count = ref(0);
// 自動推論為 ComputedRef<number>
const double = computed(() => count.value * 2);3.2 明確標註
typescript
import { computed, type ComputedRef } from 'vue';
// 標註回傳類型
const formatted = computed<string>(() => {
return `Count: ${count.value}`;
});
// 使用 ComputedRef
const result: ComputedRef<number> = computed(() => count.value * 2);3.3 可寫入 computed
typescript
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed({
get(): string {
return `${firstName.value} ${lastName.value}`;
},
set(value: string) {
const [first, last] = value.split(' ');
firstName.value = first;
lastName.value = last;
},
});四、 watch 類型
4.1 監聽 ref
typescript
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newValue, oldValue) => {
// newValue 和 oldValue 自動推論為 number
console.log(`${oldValue} -> ${newValue}`);
});4.2 監聽 reactive
typescript
import { reactive, watch } from 'vue';
interface User {
name: string;
age: number;
}
const user = reactive<User>({
name: 'John',
age: 30,
});
// 監聽整個物件
watch(user, (newValue) => {
// newValue: User
console.log(newValue.name);
});
// 監聽特定屬性
watch(
() => user.name,
(newName) => {
// newName: string
console.log(newName);
}
);4.3 監聽多個來源
typescript
import { ref, watch } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
// 元組類型自動推論
console.log(`${oldFirst} ${oldLast} -> ${newFirst} ${newLast}`);
});4.4 watchEffect
typescript
import { ref, watchEffect } from 'vue';
const count = ref(0);
const stop = watchEffect(() => {
console.log(`Count: ${count.value}`);
});
// 停止監聽
stop();五、 生命週期鉤子
5.1 基本用法
typescript
import { onMounted, onUnmounted, onUpdated } from 'vue';
onMounted(() => {
console.log('mounted');
});
onUpdated(() => {
console.log('updated');
});
onUnmounted(() => {
console.log('unmounted');
});5.2 非同步處理
typescript
import { ref, onMounted } from 'vue';
interface User {
id: number;
name: string;
}
const user = ref<User | null>(null);
const loading = ref(true);
const error = ref<Error | null>(null);
onMounted(async () => {
try {
const response = await fetch('/api/user');
user.value = await response.json();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
});六、 provide / inject
6.1 類型安全的注入
typescript
import { provide, inject, type InjectionKey } from 'vue';
// 定義 key
interface User {
id: number;
name: string;
}
const userKey: InjectionKey<User> = Symbol('user');
// 提供
provide(userKey, {
id: 1,
name: 'John',
});
// 注入
const user = inject(userKey);
// user: User | undefined
// 帶預設值
const user2 = inject(userKey, { id: 0, name: 'Guest' });
// user2: User6.2 工廠函式
typescript
import { inject, type InjectionKey } from 'vue';
interface Config {
apiUrl: string;
}
const configKey: InjectionKey<Config> = Symbol('config');
// 使用工廠函式作為預設值
const config = inject(
configKey,
() => ({
apiUrl: '/api',
}),
true
);
// config: Config七、 組合函式(Composables)
7.1 基本結構
typescript
// composables/useCounter.ts
import { ref, computed, type Ref } from 'vue';
interface UseCounterReturn {
count: Ref<number>;
double: Ref<number>;
increment: () => void;
decrement: () => void;
}
export function useCounter(initial = 0): UseCounterReturn {
const count = ref(initial);
const double = computed(() => count.value * 2);
function increment() {
count.value++;
}
function decrement() {
count.value--;
}
return {
count,
double,
increment,
decrement,
};
}7.2 使用
vue
<script setup lang="ts">
import { useCounter } from "@/composables/useCounter";
const { count, double, increment } = useCounter(10);
</script>7.3 非同步組合函式
typescript
// composables/useFetch.ts
import { ref, type Ref } from 'vue';
interface UseFetchReturn<T> {
data: Ref<T | null>;
loading: Ref<boolean>;
error: Ref<Error | null>;
refetch: () => Promise<void>;
}
export function useFetch<T>(url: string): UseFetchReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>;
const loading = ref(false);
const error = ref<Error | null>(null);
async function refetch() {
loading.value = true;
error.value = null;
try {
const response = await fetch(url);
data.value = await response.json();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
}
refetch();
return { data, loading, error, refetch };
}總結
| API | 類型標註方式 |
|---|---|
ref<T>() | 泛型參數 |
reactive<T>() | 泛型或 interface |
computed<T>() | 回傳類型 |
watch() | 自動推論 |
inject(key) | InjectionKey |
| Composables | 明確回傳類型 |
> **最佳實踐**:
- 優先讓 TypeScript 推論
- 複雜類型明確標註
- 組合函式定義回傳介面
進階挑戰
- 建立一個
useLocalStorage<T>組合函式 - 實作一個帶有類型安全的
useEventBus - 建立一個
useAsync<T>處理非同步邏輯
延伸閱讀與資源
- Vue 3 TypeScript 指南
- VueUse - 組合函式庫