各位靓仔靓女们,晚上好!我是你们的老朋友,今天要跟大家唠唠嗑,主题是 Vue 项目里 TypeScript 的那些事儿。
咱们今天不搞虚的,直接上干货,从配置到类型体操,保证你们听完之后,感觉自己也能写出高逼格的 Vue + TS 代码。
一、 TypeScript 配置:打好地基,盖摩天大楼
首先,要想玩转 Vue + TS,一个合理的 tsconfig.json
配置文件是必不可少的。它就像项目的蓝图,告诉 TypeScript 编译器该怎么理解你的代码。
{
"compilerOptions": {
"target": "esnext", // 编译目标,推荐 esnext,用最新的特性
"module": "esnext", // 模块化方案,推荐 esnext,配合现代打包工具
"moduleResolution": "node", // 模块解析策略,用 node 模式
"strict": true, // 开启严格模式,让代码更健壮
"jsx": "preserve", // JSX 处理方式,交给 Vue 处理
"sourceMap": true, // 生成 source map,方便调试
"resolveJsonModule": true, // 允许导入 JSON 模块
"esModuleInterop": true, // 允许 CommonJS 模块和 ES 模块互操作
"lib": ["esnext", "dom"], // 使用的库,esnext 包含最新的 ES 特性,dom 包含 DOM API
"baseUrl": ".", // 根路径,方便模块导入
"paths": {
"@/*": ["src/*"] // 路径别名,让导入更简洁
},
"types": ["node", "vite/client"], // 引入的类型定义,node 和 vite/client 一般是标配
"skipLibCheck": true // 跳过对声明文件的类型检查,加快编译速度
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], // 需要编译的文件
"exclude": ["node_modules"] // 排除的文件
}
重点解释几个关键配置:
strict: true
: 这玩意儿就像一个严厉的老师,开启后会进行各种类型检查,虽然一开始会让你觉得麻烦,但长期来看,可以避免很多潜在的 bug。 强烈建议开启!baseUrl
和paths
: 这两个配置可以让你在导入模块时更加方便,比如@/components/Button
代替../../components/Button
,代码瞬间清爽了不少。skipLibCheck: true
: 有些第三方库的声明文件可能不太规范,开启这个选项可以跳过对它们的检查,加快编译速度。 但是要注意,这样可能会引入一些类型错误,所以要谨慎使用。
二、 类型声明:给你的代码穿上盔甲
TypeScript 的核心就是类型声明,它就像代码的盔甲,可以保护你的代码免受各种类型错误的攻击。
1. 基础类型:
string
,number
,boolean
,null
,undefined
,symbol
,bigint
:这些都是 TypeScript 的基本类型,相信大家都已经很熟悉了。any
: 这玩意儿就像一个万能钥匙,可以表示任何类型。 但是要慎用! 滥用any
会让你失去 TypeScript 的类型检查的优势。unknown
: 和any
类似,也表示任何类型。 但是unknown
比any
更安全,因为在使用unknown
类型的变量之前,必须先进行类型断言或者类型收窄。void
: 表示没有返回值的函数。never
: 表示永远不会到达的类型,比如抛出异常的函数。
2. 对象类型:
interface User {
id: number;
name: string;
age?: number; // 可选属性
readonly email: string; // 只读属性
}
const user: User = {
id: 1,
name: "张三",
email: "[email protected]",
};
interface
: 定义接口,用来描述对象的结构。type
: 定义类型别名,可以用来表示各种类型,包括基本类型、对象类型、联合类型、交叉类型等等。- 可选属性: 用
?
表示,表示该属性可以不存在。 - 只读属性: 用
readonly
表示,表示该属性只能在对象创建时赋值,之后不能修改。
3. 数组类型:
const numbers: number[] = [1, 2, 3];
const strings: Array<string> = ["a", "b", "c"]; // 泛型写法
4. 函数类型:
// 函数声明
function add(x: number, y: number): number {
return x + y;
}
// 函数表达式
const multiply: (x: number, y: number) => number = (x, y) => x * y;
// 接口定义函数类型
interface Calculate {
(x: number, y: number): number;
}
const divide: Calculate = (x, y) => x / y;
- 函数类型声明需要指定参数类型和返回值类型。
- 可以使用接口来定义函数类型,让代码更清晰。
5. 枚举类型:
enum Color {
Red,
Green,
Blue,
}
const myColor: Color = Color.Red;
- 枚举类型可以用来定义一组相关的常量。
- 默认情况下,枚举成员的值从 0 开始递增。
- 可以手动指定枚举成员的值。
6. 联合类型和交叉类型:
// 联合类型:表示一个变量可以是多种类型之一
type StringOrNumber = string | number;
let value: StringOrNumber = "hello";
value = 123;
// 交叉类型:表示一个变量同时满足多种类型
interface Person {
name: string;
}
interface Age {
age: number;
}
type PersonWithAge = Person & Age;
const person: PersonWithAge = {
name: "李四",
age: 20,
};
- 联合类型用
|
表示,表示一个变量可以是多种类型之一。 - 交叉类型用
&
表示,表示一个变量同时满足多种类型。
三、 泛型:让你的代码更灵活
泛型就像一个占位符,可以在定义函数、接口或类的时候,不指定具体的类型,而是在使用的时候再指定。 这样可以提高代码的复用性和灵活性。
1. 泛型函数:
function identity<T>(arg: T): T {
return arg;
}
const result1 = identity<string>("hello"); // 指定类型为 string
const result2 = identity(123); // 类型推断为 number
T
就是一个类型变量,可以用来表示任何类型。- 在使用泛型函数时,可以显式地指定类型,也可以让 TypeScript 自动进行类型推断。
2. 泛型接口:
interface GenericInterface<T> {
value: T;
getValue: () => T;
}
const obj: GenericInterface<number> = {
value: 123,
getValue() {
return this.value;
},
};
3. 泛型类:
class GenericClass<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
const instance = new GenericClass<string>("hello");
4. 泛型约束:
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): number {
return arg.length;
}
const length = logLength("hello"); // string 类型有 length 属性
// const length = logLength(123); // 报错,number 类型没有 length 属性
extends
关键字可以用来约束泛型的类型范围。- 上面的例子中,
T extends Lengthwise
表示T
必须满足Lengthwise
接口,也就是必须有length
属性。
四、 Vue 项目中的 TypeScript 实践
1. 组件类型定义:
import { defineComponent } from "vue";
export default defineComponent({
name: "MyComponent",
props: {
message: {
type: String,
required: true,
},
count: {
type: Number,
default: 0,
},
},
setup(props) {
// props 的类型已经自动推断出来了
console.log(props.message);
console.log(props.count);
return {};
},
});
- 使用
defineComponent
可以获得更好的类型推断。 props
的类型可以根据type
和default
自动推断出来。
2. 使用 reactive
和 ref
定义响应式数据:
import { defineComponent, reactive, ref } from "vue";
export default defineComponent({
setup() {
const state = reactive({
name: "张三",
age: 20,
});
const count = ref(0);
function increment() {
count.value++;
}
return {
state,
count,
increment,
};
},
});
reactive
用于定义响应式的对象。ref
用于定义响应式的基本类型数据。- 需要使用
.value
来访问ref
的值。
3. 使用 computed
定义计算属性:
import { defineComponent, reactive, computed } from "vue";
export default defineComponent({
setup() {
const state = reactive({
firstName: "张",
lastName: "三",
});
const fullName = computed(() => {
return state.firstName + state.lastName;
});
return {
state,
fullName,
};
},
});
computed
可以根据其他响应式数据计算出一个新的响应式数据。computed
的返回值类型会自动推断出来。
4. 使用 watch
监听数据变化:
import { defineComponent, ref, watch } from "vue";
export default defineComponent({
setup() {
const count = ref(0);
watch(
count,
(newValue, oldValue) => {
console.log("count changed", newValue, oldValue);
},
{ immediate: true } // 立即执行一次
);
return {
count,
};
},
});
watch
可以监听一个或多个响应式数据的变化。- 可以指定
immediate: true
来立即执行一次回调函数。
5. 使用 provide
和 inject
进行依赖注入:
// 父组件
import { defineComponent, provide } from "vue";
interface MyService {
message: string;
}
const myService: MyService = {
message: "hello from parent",
};
export default defineComponent({
setup() {
provide<MyService>("myService", myService);
return {};
},
});
// 子组件
import { defineComponent, inject } from "vue";
export default defineComponent({
setup() {
const myService = inject<MyService>("myService");
if (myService) {
console.log(myService.message); // hello from parent
}
return {};
},
});
provide
用于在父组件中提供依赖。inject
用于在子组件中注入依赖。- 需要使用泛型来指定依赖的类型。
6. 事件类型定义:
import { defineComponent } from "vue";
export default defineComponent({
emits: ["update:modelValue"], // 声明组件会触发的事件
props: {
modelValue: {
type: String,
default: "",
},
},
setup(props, { emit }) {
const updateValue = (newValue: string) => {
emit("update:modelValue", newValue);
};
return {
updateValue,
};
},
});
- 使用
emits
选项来声明组件会触发的事件。 - 可以在
emit
函数中指定事件参数的类型。
五、 类型体操 (Type Challenges): 练就类型大师
光说不练假把式,要想真正掌握 TypeScript 的类型系统,还需要进行大量的练习。 Type Challenges 就是一个非常好的练习平台,它提供了各种各样的类型体操题目,可以帮助你提高类型编程的能力。
举个例子: 实现 MyPick<T, K>
MyPick<T, K>
的作用是从类型 T
中选取指定的属性 K
,返回一个新的类型。
// 实现 MyPick<T, K>
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// 测试用例
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = MyPick<Todo, "title" | "completed">;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
keyof T
表示类型T
的所有属性名组成的联合类型。K extends keyof T
表示K
必须是T
的属性名之一。[P in K]
表示遍历K
中的每一个属性名P
。T[P]
表示类型T
中属性P
的类型。
通过 Type Challenges 的练习,你可以掌握 TypeScript 的各种高级特性,比如条件类型、映射类型、 infer 等等,从而写出更加强大和灵活的代码。
六、 总结:拥抱 TypeScript,提升开发效率
TypeScript 是一门强大的语言,它可以提高代码的可维护性、可读性和可扩展性。 在 Vue 项目中,使用 TypeScript 可以让你写出更健壮、更安全的代码,从而提升开发效率。
当然,学习 TypeScript 需要时间和精力,但是一旦掌握了它,你将会发现它带来的好处是巨大的。 希望今天的分享能帮助大家更好地理解和使用 TypeScript,写出更优秀的 Vue 项目。
好了,今天的分享就到这里,谢谢大家! 咱们下期再见!