阐述 Vue 项目中 TypeScript 的最佳实践,包括配置、类型声明、泛型和类型体操 (Type Challenges)。

各位靓仔靓女们,晚上好!我是你们的老朋友,今天要跟大家唠唠嗑,主题是 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。 强烈建议开启!
  • baseUrlpaths: 这两个配置可以让你在导入模块时更加方便,比如 @/components/Button 代替 ../../components/Button,代码瞬间清爽了不少。
  • skipLibCheck: true: 有些第三方库的声明文件可能不太规范,开启这个选项可以跳过对它们的检查,加快编译速度。 但是要注意,这样可能会引入一些类型错误,所以要谨慎使用。

二、 类型声明:给你的代码穿上盔甲

TypeScript 的核心就是类型声明,它就像代码的盔甲,可以保护你的代码免受各种类型错误的攻击。

1. 基础类型:

  • string, number, boolean, null, undefined, symbol, bigint:这些都是 TypeScript 的基本类型,相信大家都已经很熟悉了。
  • any: 这玩意儿就像一个万能钥匙,可以表示任何类型。 但是要慎用! 滥用 any 会让你失去 TypeScript 的类型检查的优势。
  • unknown: 和 any 类似,也表示任何类型。 但是 unknownany 更安全,因为在使用 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 的类型可以根据 typedefault 自动推断出来。

2. 使用 reactiveref 定义响应式数据:

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. 使用 provideinject 进行依赖注入:

// 父组件
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 项目。

好了,今天的分享就到这里,谢谢大家! 咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注