跳至主要內容
Skip to content

Vue3 Props、Emit、Expose 類型

元件的介面包含 Props、Emit 和 Expose。本篇將介紹如何為它們加上完整的類型定義。


一、 defineProps 類型

1.1 執行期宣告

vue
<script setup lang="ts">
const props = defineProps({
  title: String,
  count: {
    type: Number,
    required: true,
  },
  disabled: {
    type: Boolean,
    default: false,
  },
});

// props.title: string | undefined
// props.count: number
// props.disabled: boolean
</script>

1.2 類型宣告(推薦)

vue
<script setup lang="ts">
interface Props {
  title?: string;
  count: number;
  disabled?: boolean;
}

const props = defineProps<Props>();
</script>

1.3 複雜類型

vue
<script setup lang="ts">
interface User {
  id: number;
  name: string;
}

interface Props {
  user: User;
  users: User[];
  status: "pending" | "success" | "error";
  onClick?: (id: number) => void;
}

const props = defineProps<Props>();
</script>

二、 withDefaults 預設值

2.1 基本用法

vue
<script setup lang="ts">
interface Props {
  title?: string;
  count?: number;
  disabled?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  title: "Default Title",
  count: 0,
  disabled: false,
});
</script>

2.2 複雜預設值

vue
<script setup lang="ts">
interface Props {
  items?: string[];
  user?: { name: string };
}

const props = withDefaults(defineProps<Props>(), {
  items: () => [],
  user: () => ({ name: "Guest" }),
});
</script>

IMPORTANT

陣列和物件的預設值必須使用工廠函式。


三、 defineEmits 類型

3.1 執行期宣告

vue
<script setup lang="ts">
const emit = defineEmits(["update", "delete"]);

emit("update", 123);
emit("delete");
</script>

3.2 類型宣告

vue
<script setup lang="ts">
const emit = defineEmits<{
  (e: "update", id: number): void;
  (e: "delete", id: number): void;
  (e: "submit", data: { name: string; email: string }): void;
}>();

emit("update", 123);
emit("submit", { name: "John", email: "john@example.com" });
</script>

3.3 簡化語法(Vue 3.3+)

vue
<script setup lang="ts">
const emit = defineEmits<{
  update: [id: number];
  delete: [id: number];
  submit: [data: { name: string; email: string }];
}>();
</script>

四、 v-model 類型

4.1 單一 v-model

vue
<!-- Parent -->
<script setup lang="ts">
import { ref } from "vue";
import CustomInput from "./CustomInput.vue";

const text = ref("");
</script>

<template>
  <CustomInput v-model="text" />
</template>
vue
<!-- CustomInput.vue -->
<script setup lang="ts">
const props = defineProps<{
  modelValue: string;
}>();

const emit = defineEmits<{
  "update:modelValue": [value: string];
}>();

function updateValue(e: Event) {
  emit("update:modelValue", (e.target as HTMLInputElement).value);
}
</script>

<template>
  <input :value="modelValue" @input="updateValue" />
</template>

4.2 多個 v-model

vue
<script setup lang="ts">
interface Props {
  firstName: string;
  lastName: string;
}

const props = defineProps<Props>();

const emit = defineEmits<{
  "update:firstName": [value: string];
  "update:lastName": [value: string];
}>();
</script>

<template>
  <input
    :value="firstName"
    @input="emit('update:firstName', ($event.target as HTMLInputElement).value)"
  />
  <input
    :value="lastName"
    @input="emit('update:lastName', ($event.target as HTMLInputElement).value)"
  />
</template>

4.3 使用 defineModel(Vue 3.4+)

vue
<script setup lang="ts">
const model = defineModel<string>();
// 等同於 props.modelValue + emit('update:modelValue')

// 帶預設值
const count = defineModel<number>("count", { default: 0 });
</script>

<template>
  <input v-model="model" />
</template>

五、 defineExpose 類型

5.1 暴露方法

vue
<!-- ChildComponent.vue -->
<script setup lang="ts">
import { ref } from "vue";

const count = ref(0);

function increment() {
  count.value++;
}

function reset() {
  count.value = 0;
}

defineExpose({
  count,
  increment,
  reset,
});
</script>

5.2 父元件存取

vue
<script setup lang="ts">
import { ref } from "vue";
import ChildComponent from "./ChildComponent.vue";

const childRef = ref<InstanceType<typeof ChildComponent> | null>(null);

function handleClick() {
  childRef.value?.increment();
  console.log(childRef.value?.count);
}
</script>

<template>
  <ChildComponent ref="childRef" />
  <button @click="handleClick">Increment Child</button>
</template>

5.3 定義介面

vue
<!-- ChildComponent.vue -->
<script setup lang="ts">
export interface ChildExpose {
  count: number;
  increment: () => void;
  reset: () => void;
}

defineExpose<ChildExpose>({
  count,
  increment,
  reset,
});
</script>
vue
<!-- Parent -->
<script setup lang="ts">
import type { ChildExpose } from "./ChildComponent.vue";

const childRef = ref<ChildExpose | null>(null);
</script>

六、 defineSlots 類型

6.1 定義插槽類型

vue
<script setup lang="ts">
interface User {
  id: number;
  name: string;
}

defineSlots<{
  default: (props: { message: string }) => any;
  header: () => any;
  item: (props: { user: User; index: number }) => any;
}>();
</script>

<template>
  <div>
    <slot name="header" />
    <slot :message="'Hello'" />
    <slot
      name="item"
      v-for="(user, index) in users"
      :user="user"
      :index="index"
    />
  </div>
</template>

七、 實用範例

7.1 表單元件

vue
<script setup lang="ts">
interface Props {
  label: string;
  modelValue: string;
  error?: string;
  disabled?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
  disabled: false,
});

const emit = defineEmits<{
  "update:modelValue": [value: string];
  blur: [];
}>();

function handleInput(e: Event) {
  emit("update:modelValue", (e.target as HTMLInputElement).value);
}
</script>

<template>
  <div class="form-field">
    <label>{{ label }}</label>
    <input
      :value="modelValue"
      :disabled="disabled"
      @input="handleInput"
      @blur="emit('blur')"
    />
    <span v-if="error" class="error">{{ error }}</span>
  </div>
</template>

7.2 Modal 元件

vue
<script setup lang="ts">
interface Props {
  visible: boolean;
  title?: string;
  width?: string;
}

const props = withDefaults(defineProps<Props>(), {
  title: "Modal",
  width: "500px",
});

const emit = defineEmits<{
  "update:visible": [value: boolean];
  confirm: [];
  cancel: [];
}>();

function close() {
  emit("update:visible", false);
}

function handleConfirm() {
  emit("confirm");
  close();
}
</script>

<template>
  <Teleport to="body">
    <div v-if="visible" class="modal-overlay" @click="close">
      <div class="modal" :style="{ width }" @click.stop>
        <header>{{ title }}</header>
        <main><slot /></main>
        <footer>
          <button @click="emit('cancel')">取消</button>
          <button @click="handleConfirm">確認</button>
        </footer>
      </div>
    </div>
  </Teleport>
</template>

總結

API用途類型方式
defineProps<T>()接收屬性interface
withDefaults()預設值工廠函式
defineEmits<T>()發送事件函式重載
defineModel<T>()雙向綁定泛型
defineExpose<T>()暴露方法interface
defineSlots<T>()插槽類型泛型

> **推薦做法**:

  • 使用 interface 定義 Props
  • 複雜預設值用工廠函式
  • defineModel 簡化 v-model

進階挑戰

  1. 建立一個帶有完整類型的 Select 元件
  2. 實作一個支援泛型的 List 元件
vue
<!-- 練習 2 提示 -->
<script setup lang="ts" generic="T">
const props = defineProps<{
  items: T[];
  keyField: keyof T;
}>();
</script>

延伸閱讀與資源