Vue 3 TypeScript Props 設定:withDefaults 與解構賦值
在 Vue 3 的 <script setup> 中,如果你使用 TypeScript 的介面 (Interface) 來定義 Props,你會發現傳統的 JavaScript 預設值寫法行不通。這就是 withDefaults 大顯身手的時候。
為什麼需要 withDefaults?
JavaScript vs TypeScript 的差異
在純 JavaScript 中,你可以很直覺地寫:
const props = defineProps({
title: { type: String, default: "My Profile" },
});
但在 TypeScript 的 型別宣告模式 下,當你使用 defineProps<Props>() 時:
- 不使用
withDefaults: 你只能定義title?: string(可選),但如果父組件沒傳,值就會是undefined。 - 使用
withDefaults: 你可以確保即使父組件沒傳,變數也會有一個正確的預設值。
編譯時 vs 運行時的差異
當你使用「編譯時指令」defineProps<Props>() 時,TypeScript 只能幫你檢查型別,但無法直接賦予初始值。withDefaults 就是專門用來為這些型別定義的 Props 設定預設值的工具。
特別注意: 對於陣列 (Array) 或 物件 (Object) 的預設值,必須使用一個 Factory Function (箭頭函式) 回傳值:
tabs: () => []。這是為了避免多個組件實例共用同一個記憶體位址的資料。
基本用法
方法一:定義 Interface(最推薦)
這是最乾淨的做法,將型別定義與邏輯分開。
// 1. 先定義 Props 的結構
interface Props {
title?: string; // 問號代表選填
tabs: string[]; // 強制要求字串陣列
count?: number;
}
// 2. 將 Interface 傳入 defineProps
const props = withDefaults(defineProps<Props>(), {
title: "My Profile",
tabs: () => [], // 物件或陣列預設值需用 Factory Function
count: 0,
});
方法二:直接寫在泛型內
如果 Props 很少,你也可以直接把型別寫在角括號 < > 裡面:
const props = withDefaults(
defineProps<{
title: string;
isOpen: boolean;
}>(),
{
title: "預設標題",
isOpen: false,
}
);
進階用法
使用外部型別或 Type Alias
如果你的型別非常複雜,或是在多個組件間共用,可以定義成 type:
type TabItem = {
id: number;
label: string;
};
interface Props {
title: string;
tabs: TabItem[]; // 使用自定義物件型別
}
const props = withDefaults(defineProps<Props>(), {
tabs: () => [{ id: 1, label: "Home" }],
});
從外部檔案匯入型別
在 Vue 3.3+ 之後,你已經可以從外部檔案 import 型別進來使用了:
// types/props.ts
export interface UserProps {
name?: string;
age?: number;
}
// MyComponent.vue
import type { UserProps } from "@/types/props";
const props = withDefaults(defineProps<UserProps>(), {
name: "Guest",
age: 0,
});
Vue 3.5+ 的新寫法:解構賦值(推薦)
如果你覺得 withDefaults 寫起來太囉唆,目前 Vue 3 (3.5+) 已經正式支援 「解構賦值」 來設定預設值,這是現在最推薦、也最簡潔的寫法:
// 推薦的新寫法 (Vue 3.5+)
interface Props {
title?: string;
tabs?: string[];
}
const { title = "My Profile", tabs = [] } = defineProps<Props>();
這種寫法的優點:
- 更直覺: 跟標準 JavaScript 解構語法一模一樣。
- 響應式: Vue 的編譯器會在背後處理,解構後的變數依然保有響應式(Reactive)。
- 程式碼更少: 不需要額外包一層
withDefaults。 - 陣列/物件直接寫: 不需要使用
() => []的 Factory Function,直接寫[]即可。
比較與選擇
功能比較表
| 特性 | withDefaults | 解構賦值 (Vue 3.5+) |
|---|---|---|
| 語法 | 較為冗長,需包兩層 | 簡潔,像原生 JS |
| 陣列/物件 | 必須使用 () => [] | 直接寫 [] 即可 |
| 適用版本 | Vue 3.0+ | Vue 3.5+ (目前主流) |
| 型別檢查 | ✅ 完整支援 | ✅ 完整支援 |
何時使用哪種方法?
- 使用解構賦值: 如果你的專案版本是 Vue 3.5+,這是目前最推薦的做法。
- 使用
withDefaults: 如果你的專案是舊版本(Vue 3.0 - 3.4),或公司規範要求使用此寫法。
必知的細節
選填與預設值的關係
在 interface 中,如果你打算在 withDefaults 裡給它預設值,在 Interface 裡通常會加上 ?(例如 title?: string),這樣父組件不傳東西時,TypeScript 才不會報錯。
Factory Function 的重要性
對於陣列和物件的預設值,必須使用 Factory Function,否則所有組件實例會共享同一個記憶體位址:
// ❌ 錯誤寫法
const props = withDefaults(defineProps<Props>(), {
tabs: [], // 所有實例共用同一個陣列
});
// ✅ 正確寫法
const props = withDefaults(defineProps<Props>(), {
tabs: () => [], // 每個實例都有獨立的陣列
});
複雜型別的限制
- Vue 3.2 以前:
defineProps<Props>()括號內的型別必須寫在同一個檔案。 - Vue 3.3+: 可以從外部檔案
import型別進來使用。
總結
- Vue 3.5+ 專案: 優先使用解構賦值寫法,最簡潔且功能完整。
- 舊版專案: 使用
withDefaults搭配 Interface 定義,清晰且易於維護。 - 複雜型別: 考慮將型別定義抽離成獨立的檔案,方便多個組件共用。
選擇適合你專案的寫法,確保程式碼的可維護性和可讀性!