props、emits 与注入的类型
TypeScript 与 Vue 3 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
Vue 3 对 TypeScript 的支持是一等公民,但前提是你用对写法。这一节掌握 defineProps / defineEmits 的类型语法、withDefaults 的边界、InjectionKey 的类型化,以及类型错误在哪些情况下会被运行时校验漏掉。
问题场景
组件从 JS 迁到 TS,props 改成 defineProps<{ items: Item[] }>(),类型看着都对。上线后发现父组件传了 items: undefined——TS 里 items 必填,但 JS 调用方(或模板里的可选数据)绕过了检查;另一个组件 emit 事件时参数类型写错,emit("update", "1") 传了字符串,父组件按 number 处理,运行时才炸。
核心解释
props 类型语法:
<script setup lang="ts">
interface Props {
title: string;
items: { id: number; label: string }[];
size?: "sm" | "md" | "lg";
disabled?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
size: "md",
disabled: false,
});
</script>
<template>
<section :class="`card-${props.size}`">
<h3>{{ props.title }}</h3>
<p v-if="props.disabled">已禁用</p>
</section>
</template>
要点:类型语法只做编译期检查,没有运行时校验——父组件绕过 TS(比如从 JS 调、从 JSON 数据传)时不会报错。需要运行时校验的场景(组件库、对外 API)用对象语法或两者结合(defineProps<Props>() 配合 propType 的手写校验函数)。
emits 类型语法:
const emit = defineEmits<{
(e: "update:modelValue", value: number): void;
(e: "submit", payload: { id: number; title: string }): void;
}>();
这种「调用签名」写法在调用 emit("update:modelValue", "x") 时会直接报类型错误,是最该用 TS 的地方——事件参数错了最难在运行时发现。
模板 ref 的类型:
import { ref } from "vue";
const inputRef = ref<HTMLInputElement | null>(null);
function focus() {
inputRef.value?.focus();
}
模板里使用 prop 不需要写 props. 前缀,脚本里则必须通过返回的 props 对象访问,两种写法不要混。
不写泛型时是 Ref<null>,inputRef.value.focus() 会报错——这其实是好事,强迫你处理「还没挂载」的情况。
provide/inject 类型:用 InjectionKey<T> 把 key 和类型绑定(V09 演示过),注入处自动推断,找不到时返回 T | undefined,强迫你处理缺失分支。
computed 与函数返回值:computed<number>(() => ...) 通常不需要显式泛型,推断已足够;但返回值可能是联合类型时显式标注能防止意外拓宽。
示例拆解
一个类型完整的表单字段组件,覆盖 props / emits / v-model:
<script setup lang="ts">
interface Props {
modelValue: string;
label: string;
error?: string;
maxLength?: number;
}
const props = withDefaults(defineProps<Props>(), { error: "", maxLength: 120 });
const emit = defineEmits<{
(e: "update:modelValue", value: string): void;
}>();
function onInput(event: Event) {
const target = event.target as HTMLInputElement;
emit("update:modelValue", target.value);
}
</script>
<template>
<label class="field">
<span>{{ props.label }}</span>
<input
:value="props.modelValue"
:maxlength="props.maxLength"
:aria-invalid="!!props.error"
@input="onInput"
/>
<small v-if="props.error" class="error">{{ props.error }}</small>
</label>
</template>
父组件写 <TextField v-model="email" label="邮箱" :error="errors.email" />,TS 会检查 v-model 绑定值的类型与 update:modelValue 的参数类型是否一致。把 modelValue 改成 number 而忘了改 emit,立刻红。
动手任务
- 把 V04 的 TodoItem 改成 TS 版本:
todo用接口定义,emits 用调用签名声明。 - 给一个组件加
size?: "sm" | "md" | "lg",验证父组件传size="xl"时编辑器/构建报错。 - 故意从模板传
undefined给必填 prop,验证类型语法是否拦得住(多半拦不住),记录结论。
常见错误
- 以为类型语法有运行时校验:它只存在于编译期。需要运行时保护就加对象语法或手动校验。
withDefaults对函数类型默认值写错:onChange: () => {}会被当作「返回函数的默认值」;应写onChange: () => () => {}或直接用可选 prop。- 模板 ref 不写泛型:
ref(null)推断为Ref<null>,后续赋值/调用全报错。 defineEmits用数组语法:defineEmits(["update"])没有参数类型,等于放弃检查。用调用签名。
检查与复盘
- 能写出带 withDefaults 的类型化 props 与调用签名 emits
- 能解释类型语法与运行时校验的边界
- 能说出模板 ref 泛型的必要性
参考资料
- Vue 官方文档「TypeScript 总览」:项目配置与工具链
- Vue 官方文档「组合式 API 与 TS」:props/emits/ref 的类型写法
读完这一课了?
标记完成后,工作台会安排接下来的学习。