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