各位靓仔靓女,欢迎来到今天的“Vue 3 + TypeScript:类型安全的组件炼成术”讲座现场!今天咱们不搞虚的,直接上干货,教大家如何用TypeScript武装你的Vue组件,让v-model和props都乖乖听话,再也不怕类型错误搞事情!
第一章:开胃小菜:TypeScript 和 Vue 3 的基情碰撞
首先,咱们得明白TypeScript和Vue 3这对CP为啥这么受欢迎。简单来说,TypeScript就是给JavaScript加了个“类型警察”,在编译阶段就帮你揪出类型错误,省得运行时才发现,那可就晚了。而Vue 3呢,本身就是用TypeScript重写的,对TypeScript的支持简直不要太好,简直是天生一对,地设一双。
第二章:Props:组件的“家当”,类型必须安排得明明白白
Props,组件的“家当”,是父组件传递给子组件的数据。TypeScript就是要确保这些“家当”的类型正确无误。
-
基础类型:简单粗暴,直接定义
最简单的场景,比如传递一个字符串、数字或者布尔值。
// MyComponent.vue <script setup lang="ts"> import { defineProps } from 'vue'; const props = defineProps({ name: { type: String, required: true, }, age: { type: Number, default: 18, }, isAdult: { type: Boolean, default: false, }, }); </script> <template> <div> <p>Name: {{ props.name }}</p> <p>Age: {{ props.age }}</p> <p>Is Adult: {{ props.isAdult }}</p> </div> </template>
这里,
defineProps
接收一个对象,每个属性对应一个prop。type
字段指定了prop的类型,required
表示是否是必需的,default
则定义了默认值。 注意,type
的值必须是String
,Number
,Boolean
,Array
,Object
,Date
,Function
,Symbol
。 -
高级类型:对象、数组、自定义类型,一个都不能少
如果prop是对象或数组,或者你想用更复杂的类型,那就需要用到TypeScript的类型定义了。
// MyComponent.vue <script setup lang="ts"> import { defineProps, PropType } from 'vue'; interface User { id: number; name: string; email: string; } const props = defineProps({ user: { type: Object as PropType<User>, required: true, }, hobbies: { type: Array as PropType<string[]>, default: () => [], }, }); </script> <template> <div> <p>User Name: {{ props.user.name }}</p> <p>Hobbies: {{ props.hobbies.join(', ') }}</p> </div> </template>
这里,我们定义了一个
User
接口,然后使用PropType<User>
来指定user
prop的类型。对于数组,我们使用PropType<string[]>
来指定hobbies
prop的类型。 注意,as PropType<T>
这种写法是告诉TypeScript,我们知道这个prop的类型是什么。 如果想默认值是空数组或者空对象,需要用函数返回,不然所有组件实例都会共享同一个数组/对象,修改一个实例,其他的都跟着变,这可不是我们想要的。 -
联合类型和字面量类型:让类型更精确
有时候,prop的类型可能不止一种,或者只能是几个固定的值,这时候就可以用到联合类型和字面量类型了。
// MyComponent.vue <script setup lang="ts"> import { defineProps, PropType } from 'vue'; type Size = 'small' | 'medium' | 'large'; const props = defineProps({ size: { type: String as PropType<Size>, default: 'medium', validator: (value: string): boolean => { return ['small', 'medium', 'large'].includes(value); }, }, status: { type: [String, Number] as PropType<string | number>, default: 'pending', }, }); </script> <template> <div> <p>Size: {{ props.size }}</p> <p>Status: {{ props.status }}</p> </div> </template>
这里,
Size
是一个字面量类型,只能是'small'
、'medium'
或'large'
中的一个。status
是一个联合类型,可以是字符串或数字。 注意,validator
函数可以用来验证prop的值是否合法,在开发阶段会发出警告。 -
使用
defineProps
的泛型写法defineProps
还可以使用泛型写法,这种方式更加简洁,类型推断也更强大。// MyComponent.vue <script setup lang="ts"> interface Props { name: string; age?: number; isAdult?: boolean; } const props = defineProps<Props>(); </script> <template> <div> <p>Name: {{ props.name }}</p> <p>Age: {{ props.age }}</p> <p>Is Adult: {{ props.isAdult }}</p> </div> </template>
这种写法直接定义了一个
Props
接口,然后将它作为defineProps
的泛型参数。 可选的prop可以使用?
标记。
第三章:v-model:数据的“双向通道”,类型也要畅通无阻
v-model,数据的“双向通道”,是Vue中实现父子组件数据同步的重要机制。TypeScript当然也要保证这个通道的类型畅通无阻。
-
基础 v-model:简单的数据同步
最简单的v-model,就是同步一个字符串、数字或者布尔值。
// MyInput.vue (子组件) <script setup lang="ts"> import { defineProps, defineEmits, ref } from 'vue'; const props = defineProps({ modelValue: { type: String, default: '', }, }); const emit = defineEmits(['update:modelValue']); const inputValue = ref(props.modelValue); const handleChange = (event: Event) => { const target = event.target as HTMLInputElement; inputValue.value = target.value; emit('update:modelValue', target.value); }; </script> <template> <input type="text" :value="inputValue" @input="handleChange" /> </template> // ParentComponent.vue (父组件) <template> <MyInput v-model="message" /> <p>Message: {{ message }}</p> </template> <script setup lang="ts"> import { ref } from 'vue'; import MyInput from './MyInput.vue'; const message = ref(''); </script>
这里,子组件
MyInput
定义了一个modelValue
prop,用于接收父组件传递的值。同时,它还定义了一个update:modelValue
事件,用于向父组件更新值。父组件使用v-model="message"
将message
变量与子组件的modelValue
prop 和update:modelValue
事件绑定起来,实现了数据的双向同步。 -
自定义 v-model:更灵活的数据同步
有时候,我们可能需要自定义v-model的prop和事件名,比如同步一个对象的某个属性。
// MyComponent.vue (子组件) <script setup lang="ts"> import { defineProps, defineEmits } from 'vue'; interface Data { name: string; age: number; } const props = defineProps({ title: { type: String, default: '', }, modelValue: { type: Object as PropType<Data>, required: true, }, }); const emit = defineEmits(['update:modelValue', 'update:title']); const handleNameChange = (event: Event) => { const target = event.target as HTMLInputElement; emit('update:modelValue', { ...props.modelValue, name: target.value }); }; const handleTitleChange = (event: Event) => { const target = event.target as HTMLInputElement; emit('update:title', target.value); } </script> <template> <div> <input type="text" :value="props.modelValue.name" @input="handleNameChange" /> <input type="text" :value="props.title" @input="handleTitleChange" /> </div> </template> // ParentComponent.vue (父组件) <template> <MyComponent v-model:model-value="data" v-model:title="title"/> <p>Name: {{ data.name }}</p> <p>Age: {{ data.age }}</p> <p>Title: {{ title }}</p> </template> <script setup lang="ts"> import { ref } from 'vue'; import MyComponent from './MyComponent.vue'; interface Data { name: string; age: number; } const data = ref<Data>({ name: 'John', age: 30 }); const title = ref('Mr.'); </script>
这里,我们使用
v-model:model-value="data"
和v-model:title="title"
来指定v-model的prop和事件名。子组件需要定义相应的prop和事件,才能实现数据的双向同步。 -
多 v-model:多个数据同步
一个组件可以同时使用多个 v-model。
// MyComponent.vue (子组件) <script setup lang="ts"> import { defineProps, defineEmits } from 'vue'; const props = defineProps({ name: { type: String, default: '', }, age: { type: Number, default: 0, }, }); const emit = defineEmits(['update:name', 'update:age']); const handleNameChange = (event: Event) => { const target = event.target as HTMLInputElement; emit('update:name', target.value); }; const handleAgeChange = (event: Event) => { const target = event.target as HTMLInputElement; emit('update:age', Number(target.value)); }; </script> <template> <div> <input type="text" :value="props.name" @input="handleNameChange" /> <input type="number" :value="props.age" @input="handleAgeChange" /> </div> </template> // ParentComponent.vue (父组件) <template> <MyComponent v-model:name="name" v-model:age="age" /> <p>Name: {{ name }}</p> <p>Age: {{ age }}</p> </template> <script setup lang="ts"> import { ref } from 'vue'; import MyComponent from './MyComponent.vue'; const name = ref(''); const age = ref(0); </script>
每个
v-model
指令都绑定到不同的 prop 和事件。
第四章:Emit:组件的“汇报”,类型也要一丝不苟
Emit,组件的“汇报”,是子组件向父组件传递数据的手段。TypeScript当然也要确保这些“汇报”的类型正确无误。
// MyComponent.vue
<script setup lang="ts">
import { defineEmits } from 'vue';
// 使用类型字面量定义 emits
const emit = defineEmits<{
(e: 'custom-event', payload: { id: number; name: string }): void
(e: 'another-event', value: string): void
}>()
function doSomething() {
emit('custom-event', { id: 123, name: 'foobar' })
emit('another-event', 'hello')
}
// 另一种写法,使用数组定义
const emit2 = defineEmits(['custom-event', 'another-event'])
function doSomething2() {
emit2('custom-event', { id: 123, name: 'foobar' }) // 不会报错,但类型检查较弱
emit2('another-event', 'hello')
}
</script>
-
使用泛型语法
// MyComponent.vue <script setup lang="ts"> import { defineEmits } from 'vue'; interface Emits { (e: 'update:name', value: string): void (e: 'update:age', value: number): void } const emit = defineEmits<Emits>(); const handleNameChange = (event: Event) => { const target = event.target as HTMLInputElement; emit('update:name', target.value); }; const handleAgeChange = (event: Event) => { const target = event.target as HTMLInputElement; emit('update:age', Number(target.value)); }; </script>
这种写法更加清晰,易于维护。
第五章:类型推断:让 TypeScript 自动搞定
TypeScript的类型推断能力非常强大,很多时候它可以自动推断出类型,省去我们手动声明的麻烦。
-
利用
computed
和ref
的类型推断// MyComponent.vue <script setup lang="ts"> import { ref, computed } from 'vue'; const count = ref(0); // TypeScript 会自动推断出 count 的类型是 Ref<number> const doubledCount = computed(() => count.value * 2); // TypeScript 会自动推断出 doubledCount 的类型是 ComputedRef<number> </script> <template> <div> <p>Count: {{ count }}</p> <p>Doubled Count: {{ doubledCount }}</p> </div> </template>
这里,我们没有显式地声明
count
和doubledCount
的类型,但是TypeScript会自动推断出来。 -
利用函数返回值类型推断
// MyComponent.vue <script setup lang="ts"> function getMessage() { return 'Hello, world!'; // TypeScript 会自动推断出 getMessage 的返回值类型是 string } const message = getMessage(); // TypeScript 会自动推断出 message 的类型是 string </script> <template> <div> <p>{{ message }}</p> </div> </template>
这里,我们没有显式地声明
getMessage
的返回值类型,但是TypeScript会自动推断出来。
第六章:实战演练:一个完整的例子
咱们来一个完整的例子,把上面讲的知识都用上。
// components/UserForm.vue
<template>
<form @submit.prevent="handleSubmit">
<div>
<label for="name">Name:</label>
<input type="text" id="name" v-model="form.name" />
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" v-model="form.email" />
</div>
<button type="submit">Submit</button>
</form>
</template>
<script setup lang="ts">
import { reactive, defineEmits } from 'vue';
interface UserForm {
name: string;
email: string;
}
const emit = defineEmits<{
(e: 'submit', user: UserForm): void
}>()
const form = reactive<UserForm>({
name: '',
email: '',
});
const handleSubmit = () => {
emit('submit', { ...form });
};
</script>
// App.vue
<template>
<UserForm @submit="handleUserSubmit" />
<p>User Name: {{ user?.name }}</p>
<p>User Email: {{ user?.email }}</p>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import UserForm from './components/UserForm.vue';
interface User {
name: string;
email: string;
}
const user = ref<User | null>(null);
const handleUserSubmit = (userData: User) => {
user.value = userData;
};
</script>
这个例子展示了如何使用TypeScript来定义props、v-model和emit的类型,以及如何利用类型推断来简化代码。
第七章:疑难解答:常见问题和解决方案
-
类型错误提示太多,眼花缭乱怎么办?
- 仔细阅读错误提示,理解错误的含义。
- 逐步排查,先解决最明显的错误。
- 善用搜索引擎,查阅相关资料。
- 如果实在解决不了,可以向社区求助。
-
类型定义太繁琐,代码变得臃肿怎么办?
- 合理利用类型推断,减少手动声明的类型。
- 使用类型别名和接口,简化类型定义。
- 将类型定义放在单独的文件中,提高代码的可读性。
-
如何处理第三方库的类型定义?
- 安装对应的
@types
包。 - 如果找不到
@types
包,可以尝试自己编写类型定义。 - 可以使用
any
类型来临时解决问题,但要尽量避免使用。
- 安装对应的
第八章:总结与展望
今天我们学习了如何使用TypeScript为Vue组件编写类型安全的props、v-model和emit。掌握这些技巧,可以大大提高代码的质量和可维护性,减少运行时错误的发生。当然,TypeScript的学习是一个持续的过程,希望大家在实践中不断探索,不断进步!
最后,给大家留个小作业:尝试将你现有的Vue组件用TypeScript重构一遍,看看能发现多少隐藏的bug!
今天的讲座就到这里,感谢大家的收听!咱们下期再见!