大家好!欢迎来到今天的Vue 3源码极客小课堂。我是你们的老朋友,今天咱们要聊聊Vue 3中 TypeScript 的那些事儿,重点是怎样给 props
和 emits
安排明白的类型定义。说白了,就是怎么让我们的组件既能高效工作,又能让 TypeScript 安心,不再报错。
先别紧张,虽然听起来有点“极客”,但实际上没那么难。咱们争取用最接地气的方式,把这事儿说明白。
开胃小菜:为什么要类型定义?
在深入 props
和 emits
之前,先简单聊聊为什么要用 TypeScript。如果你已经对 TypeScript 的好处了如指掌,可以跳过这部分。
想象一下,你写了一个组件,需要接收一个 name
属性,然后兴高采烈地用了它。结果呢?
- 某一天,你或者你的同事手一抖,把
name
传成了数字123
。 - 或者,你以为组件会 emit 一个
update
事件,结果吭哧吭哧写了一堆代码,最后发现组件根本没这个事件。
这种时候,如果没有类型检查,你可能要等到运行时才会发现问题,到时候 debug 起来,那叫一个酸爽。
TypeScript 的出现,就是为了避免这种尴尬。它就像一个尽职尽责的门卫,在你写代码的时候,就帮你检查类型是否匹配,提前发现潜在的错误。
props
的类型定义:三种姿势,各有千秋
在 Vue 3 中,定义 props
的类型,主要有三种方式:
- 运行时声明 (Runtime Props Declaration)
- 基于类型的声明 (Type-Based Props Declaration)
- 混合方式 (Hybrid Approach)
咱们一个个来看。
1. 运行时声明 (Runtime Props Declaration)
这是 Vue 2 中最常见的写法,在 Vue 3 中依然可用。它的核心思想是,在 props
选项中,使用对象字面量来声明属性,并指定它们的类型。
import { defineComponent } from 'vue';
export default defineComponent({
props: {
name: {
type: String,
required: true,
},
age: {
type: Number,
default: 18,
},
isActive: {
type: Boolean,
default: false,
},
//允许传入多种类型
info:{
type:[String,Number]
}
},
setup(props) {
console.log(props.name); // 类型推断为 string
console.log(props.age); // 类型推断为 number
console.log(props.isActive); // 类型推断为 boolean
// props.name = 'new name'; // 错误!props 是只读的
return {};
},
});
这种方式的优点是简单直观,易于理解。缺点是,类型信息是在运行时声明的,TypeScript 只能根据 type
属性进行有限的类型推断。
2. 基于类型的声明 (Type-Based Props Declaration)
这是 Vue 3 推荐的写法,它充分利用了 TypeScript 的类型系统,可以提供更精确的类型检查和更好的代码提示。
import { defineComponent, PropType } from 'vue';
interface Props {
name: string;
age?: number; // 可选属性
isActive: boolean;
profile: {
nickname: string;
level: number;
}
}
export default defineComponent({
props: {
name: {
type: String as PropType<string>, // as PropType<string> 很重要
required: true,
},
age: {
type: Number as PropType<number>,
default: 18,
},
isActive: {
type: Boolean as PropType<boolean>,
default: false,
},
profile:{
type: Object as PropType<Props['profile']>,
required: true
}
},
setup(props) {
console.log(props.name); // 类型推断为 string
console.log(props.age); // 类型推断为 number
console.log(props.isActive); // 类型推断为 boolean
console.log(props.profile.nickname); // 类型推断为 string
return {};
},
});
在这个例子中,我们首先定义了一个 Props
接口,用来描述组件的 props
的类型。然后,在 props
选项中,我们使用 as PropType<类型>
来明确指定每个属性的类型。
重点:as PropType<类型>
这个 as PropType<类型>
非常重要! 它可以告诉 TypeScript,type
属性对应的类型是什么。如果没有它,TypeScript 可能无法正确推断类型,导致类型检查失效。
更简洁的写法:defineProps
(从 Vue 3.3 开始)
Vue 3.3 引入了一个新的 API:defineProps
。它可以让你更简洁地定义 props
的类型。
import { defineComponent, defineProps } from 'vue';
interface Props {
name: string;
age?: number; // 可选属性
isActive: boolean;
profile: {
nickname: string;
level: number;
}
}
export default defineComponent({
setup() {
const props = defineProps<Props>();
console.log(props.name); // 类型推断为 string
console.log(props.age); // 类型推断为 number
console.log(props.isActive); // 类型推断为 boolean
console.log(props.profile.nickname); // 类型推断为 string
return {};
},
});
使用 defineProps
,你可以直接传入一个类型参数,TypeScript 会自动推断 props
的类型。这种方式更加简洁,也更符合 TypeScript 的编程习惯。
3. 混合方式 (Hybrid Approach)
有时候,你可能需要结合运行时声明和基于类型的声明。比如,你希望使用运行时声明来指定 default
值,同时又希望利用 TypeScript 进行类型检查。
import { defineComponent, PropType } from 'vue';
interface Props {
name: string;
age?: number;
}
export default defineComponent({
props: {
name: {
type: String as PropType<string>,
required: true,
},
age: {
type: Number as PropType<number>,
default: 18, // 运行时声明
},
},
setup(props) {
console.log(props.name); // 类型推断为 string
console.log(props.age); // 类型推断为 number
return {};
},
});
这种方式比较灵活,可以根据实际情况选择合适的声明方式。
emits
的类型定义:事件也要安排明白
接下来,咱们来看看 emits
的类型定义。和 props
类似,emits
也有两种主要的定义方式:
- 运行时声明 (Runtime Emits Declaration)
- 基于类型的声明 (Type-Based Emits Declaration)
1. 运行时声明 (Runtime Emits Declaration)
这种方式和 props
的运行时声明类似,使用对象字面量来声明事件,并指定它们的类型。
import { defineComponent } from 'vue';
export default defineComponent({
emits: ['update', 'delete', 'custom-event'],
setup(props, { emit }) {
emit('update', 123); // 触发 update 事件,传递一个 number 类型的值
emit('delete'); // 触发 delete 事件,不传递任何值
emit('custom-event', 'hello', 456); //触发自定义事件
return {};
},
});
这种方式的优点是简单易懂。缺点是,TypeScript 无法对事件的参数进行类型检查。也就是说,即使你传递了错误的参数类型,TypeScript 也不会报错。
2. 基于类型的声明 (Type-Based Emits Declaration)
为了解决运行时声明的不足,Vue 3 提供了基于类型的 emits
声明方式。
import { defineComponent, EmitsOptions } from 'vue';
interface Emits {
(e: 'update', value: number): void;
(e: 'delete'): void;
(e: 'custom-event', message: string, id: number): void;
}
export default defineComponent({
emits: ['update', 'delete', 'custom-event'], // 运行时声明,用于注册事件
setup(props, { emit }) {
const emitTyped = emit as Emits; // 类型断言
emitTyped('update', 123); // 正确
// emitTyped('update', 'abc'); // 错误!参数类型不匹配
emitTyped('delete'); // 正确
emitTyped('custom-event', 'hello', 456); //正确
// emitTyped('custom-event', 123, 'abc'); //错误! 参数类型不匹配
return {};
},
});
在这个例子中,我们首先定义了一个 Emits
接口,用来描述组件可以 emit 的事件类型。然后,在 setup
函数中,我们将 emit
函数类型断言为 Emits
类型。这样,TypeScript 就可以对事件的参数进行类型检查了。
更简洁的写法:defineEmits
(从 Vue 3.3 开始)
和 defineProps
类似,Vue 3.3 也引入了一个新的 API:defineEmits
。它可以让你更简洁地定义 emits
的类型。
import { defineComponent, defineEmits } from 'vue';
interface Emits {
(e: 'update', value: number): void;
(e: 'delete'): void;
(e: 'custom-event', message: string, id: number): void;
}
export default defineComponent({
setup() {
const emit = defineEmits<Emits>();
emit('update', 123); // 正确
// emit('update', 'abc'); // 错误!参数类型不匹配
emit('delete'); // 正确
emit('custom-event', 'hello', 456); //正确
//emit('custom-event', 123, 'abc'); //错误! 参数类型不匹配
return {};
},
});
使用 defineEmits
,你可以直接传入一个类型参数,TypeScript 会自动推断 emit
函数的类型。这种方式更加简洁,也更符合 TypeScript 的编程习惯。
更进一步:验证 emits
的参数
Vue 3 允许你通过 emits
选项中的函数来验证事件的参数。这可以进一步提高代码的可靠性。
import { defineComponent } from 'vue';
export default defineComponent({
emits: {
update: (value: number) => {
// 验证 value 是否是数字类型
return typeof value === 'number';
},
'custom-event': (message: string, id: number) => {
// 验证 message 是否是字符串类型,id 是否是数字类型
return typeof message === 'string' && typeof id === 'number';
},
delete:null
},
setup(props, { emit }) {
emit('update', 123); // 正确
// emit('update', 'abc'); // 错误!参数验证失败
emit('delete'); // 正确
emit('custom-event', 'hello', 456); //正确
//emit('custom-event', 123, 'abc'); //错误! 参数验证失败
return {};
},
});
在这个例子中,我们为 update
事件定义了一个验证函数,它会检查 value
是否是数字类型。如果验证失败,Vue 会在控制台中发出警告。
总结:选择最适合你的方式
总的来说,props
和 emits
的类型定义方式有很多种,你可以根据自己的需求和偏好选择最适合你的方式。
- 运行时声明: 简单易懂,但类型检查能力有限。
- 基于类型的声明: 可以提供更精确的类型检查和更好的代码提示,但需要更多的代码。
- 混合方式: 灵活,可以结合运行时声明和基于类型的声明的优点。
defineProps
和defineEmits
: Vue 3.3 引入的新 API,更加简洁,也更符合 TypeScript 的编程习惯。
特性 | 运行时声明 | 基于类型的声明 | 混合方式 | defineProps/defineEmits(Vue 3.3+) |
---|---|---|---|---|
类型检查 | 有限的类型检查(基于 type 属性) |
更精确的类型检查(基于 TypeScript 类型系统) | 灵活,可以结合两种方式的优点 | 非常精确,完全依赖 TypeScript 类型系统 |
代码简洁性 | 相对简单 | 相对复杂 | 中等 | 非常简洁 |
适用场景 | 小型项目,或者对类型检查要求不高的场景 | 中大型项目,或者对类型检查要求较高的场景 | 需要同时使用运行时声明和基于类型的声明的场景 | 推荐使用,尤其是对于大型项目,可以提升开发效率和代码质量 |
优点 | 易于理解,上手快 | 类型安全,代码提示,可维护性高 | 灵活,可以根据实际情况选择合适的声明方式 | 简洁,类型安全,代码提示,可维护性高 |
缺点 | 类型检查能力有限 | 代码量相对较多 | 可能导致代码结构混乱 | 需要对 TypeScript 有一定的了解 |
示例代码 (props) | props: { name: { type: String } } |
props: { name: { type: String as PropType<string> } } |
props: { name: { type: String as PropType<string>, default: 'default name' } } (运行时声明 default,类型声明类型) |
const props = defineProps<{ name: string }>() |
示例代码 (emits) | emits: ['update'] |
emits: { update: (value: number) => boolean } |
不适用 (混合方式主要用于props) | const emit = defineEmits<{ (e: 'update', value: number): void }>() |
最后的叮嘱:实践出真知
好了,今天的 Vue 3 源码极客小课堂就到这里了。希望通过今天的讲解,你对 props
和 emits
的类型定义有了更深入的理解。
记住,纸上得来终觉浅,绝知此事要躬行。只有在实际项目中不断练习,才能真正掌握这些知识。
祝大家编程愉快,bug 远离!我们下期再见!