Skip to main content

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>();

這種寫法的優點:

  1. 更直覺: 跟標準 JavaScript 解構語法一模一樣。
  2. 響應式: Vue 的編譯器會在背後處理,解構後的變數依然保有響應式(Reactive)。
  3. 程式碼更少: 不需要額外包一層 withDefaults
  4. 陣列/物件直接寫: 不需要使用 () => [] 的 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 定義,清晰且易於維護。
  • 複雜型別: 考慮將型別定義抽離成獨立的檔案,方便多個組件共用。

選擇適合你專案的寫法,確保程式碼的可維護性和可讀性!