跳至主要內容
Skip to content

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: User

6.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 推論
  • 複雜類型明確標註
  • 組合函式定義回傳介面

進階挑戰

  1. 建立一個 useLocalStorage<T> 組合函式
  2. 實作一個帶有類型安全的 useEventBus
  3. 建立一個 useAsync<T> 處理非同步邏輯

延伸閱讀與資源