Skip to content

TypeScript 实战进阶技巧

一、 第三方库的类型处理技巧

核心目标:准确获取并使用第三方库(如 vue-router, pinia)暴露出的方法、组件或对象的 TypeScript 类型,以增强代码的健壮性和开发效率。

1. 核心技巧:类型定义跳转

在 VS Code 中,按住 Ctrl 键并将鼠标悬停在某个方法或变量上,它会高亮并显示其类型信息。此时单击鼠标左键,可以直接跳转到该类型的定义文件 (.d.ts 文件)。这是探索任何库类型的最快方法。

好处: 无需查阅文档,即可快速了解函数需要什么参数、返回什么类型,以及对象的具体结构。

2. 实战案例 1:vue-router

场景:封装一个动态添加路由的辅助函数 addRouteFn,需要确保传入的路由配置和路由实例类型正确。

问题:如何获取 vue-router 中“单个路由规则”和“路由实例”的类型?

解决方案

  1. 探索类型:通过 Ctrl + Click 跳转到 createRouter 方法的定义,可以发现:

    • 它接收一个 RouterOptions 类型的参数。
    • 它返回一个 Router 类型的实例(这就是路由实例的类型)。
    • RouterOptions 中,routes 属性的类型是 readonly RouteRecordRaw[],因此 RouteRecordRaw 就是单个路由规则的类型。
  2. 导入并使用类型:使用 import type 语法从库中导入找到的类型。import type 是一个性能优化,它确保导入的类型在编译成 JavaScript 后会被完全移除。

typescript
// 导入 vue-router 中的类型
import type { RouteRecordRaw, Router } from 'vue-router';

// 封装一个动态添加路由的辅助函数
const addRouteFn = (routeConfig: RouteRecordRaw, router: Router) => {
  router.addRoute(routeConfig);
  // ... 其他逻辑
};

// 使用示例
// 正确使用时,类型系统会提供智能提示和检查
addRouteFn(
  {
    path: '/profile',
    name: 'profile',
    component: () => import('../views/Profile.vue'),
  },
  router // 假设 router 是一个已创建的 Router 实例
);

// 错误使用时,会立即得到编译错误
addRouteFn(
  {
    // 假设属性名拼写错误,如 passs 而不是 path
    passs: '/profile', // ❌ 错误:对象字面量只能指定已知属性,'passs' 不存在于类型 'RouteRecordRaw' 中。
  },
  router
);

收益: 通过为函数参数指定从库中获取的精确类型,可以防止运行时因数据结构错误导致的 bug,并为其他开发者提供清晰的函数使用说明。

3. 实战案例 2:pinia

场景:作为项目负责人,需要为团队成员定义一个 UserInfo Pinia store 的标准模板,强制要求该 store 必须包含一个响应式的 userInfo 数据和一个 setUserInfo 方法。

问题:如何创建一个类型来约束 defineStoresetup 函数的返回值?

解决方案

  1. 定义数据和 Store 结构类型

    • 首先,创建 userInfo 对象自身的类型接口 UserInfoType
    • 接着,创建 Store 的类型 UserInfoStoreType,它描述了 setup 函数必须返回的对象的结构。
    • 要表示响应式数据,需要 Ref 类型。通过 Ctrl + Click 查看 ref() 函数,可知其返回类型为 Ref<T>
  2. 编写类型定义

typescript
import type { Ref } from 'vue';

// 1. 定义用户信息的结构
export interface UserInfoType {
  nickname: string;
  avatar: string;
}

// 2. 定义 Store setup 函数的返回值类型
// 强制要求必须返回一个 Ref<UserInfoType> 和一个指定签名的函数
type UserInfoStoreType = () => {
  userInfo: Ref<UserInfoType>;
  setUserInfo: (info: UserInfoType) => void;
};
  1. 应用类型约束:在 defineStore 时,将这个类型应用到 setup 函数上。
typescript
import { defineStore } from 'pinia';
import { ref } from 'vue';
import type { UserInfoType, UserInfoStoreType } from './types'; // 假设类型定义在 types.ts

// 使用我们定义的类型来约束 setup 函数
export const useUserInfoStore = defineStore('userInfo', (): ReturnType<UserInfoStoreType> => {
  // 定义 state
  const userInfo = ref<UserInfoType>({ nickname: '', avatar: '' });

  // 定义 action
  const setUserInfo = (info: UserInfoType) => {
    userInfo.value = info;
  };

  // 返回 state 和 action
  // 如果返回的对象结构不符合 UserInfoStoreType,TypeScript 会报错
  return {
    userInfo,
    setUserInfo,
  };
});

收益: 这种方式为团队协作提供了强有力的保障。

  • 如果开发者忘记返回某个属性(如 setUserInfo),会报错。
  • 如果 userInfo 没有使用 ref() 进行包装,会报错。
  • 如果 setUserInfo 方法的参数或返回值类型错误,会报错。 从而确保了所有 Store 的实现都遵循统一的设计规范。

二、 基于已有类型快速创建新类型

核心目标:避免重复编写类型定义,通过 TypeScript 内置的工具类型(Utility Types)和操作符,基于已有的“单一事实来源”(如 API 返回类型),派生出所有需要的子类型。

1. 技巧 1:索引访问类型 (Indexed Access Types)

场景:API 返回一个包含多个字段的大对象(如用户信息 myInfo 和粉丝列表 fansList),你需要将这些字段分别存入不同的 ref 变量中。

问题:如何为 fansListmyInforef 提供精确的类型,而无需手动复制粘贴 API_Res 中的部分类型定义?

解决方案:使用 Type['key'] 语法直接从父类型中提取指定属性的类型。

typescript
// 1. 定义 API 返回值的完整类型(单一事实来源)
interface API_Res {
  success: boolean;
  data: {
    fansList: {
      name: string;
      followYear: string;
    }[];
    myInfo: {
      name: string;
      type: string;
    };
  };
}

// 2. 使用索引访问类型来为 ref 提供精确类型
// 错误示例:直接用空数组初始化,ts会推断为 never[] 类型
// const fansList = ref([]); // fansList 的类型被推断为 never[]
// fansList.value = res.data.fansList; // ❌ 错误:不能将 'fansList' 类型分配给 'never[]'

// 正确做法:
import { ref } from 'vue';
import type { API_Res } from './apiTypes';

// 从 API_Res 中提取出 fansList 和 myInfo 的类型
const fansList = ref<API_Res['data']['fansList']>([]);
const myInfo = ref<API_Res['data']['myInfo']>({ name: '', type: '' });

// 之后赋值就不会有任何类型错误
// fansList.value = res.data.fansList;
// myInfo.value = res.data.myInfo;

收益: 代码更简洁且易于维护。当 API 结构变更时,只需修改 API_Res 这一个地方,所有派生出来的类型都会自动更新。

2. 技巧 2:typeofkeyof 组合

场景:某个字段(如用户类型 type)的值是固定的几个字符串之一,这些值通常会定义成一个常量对象或枚举以便管理。

问题:如何创建一个代表“这几个固定字符串之一”的联合类型,并且当常量对象更新时,该类型能自动同步?

解决方案:结合使用 typeofkeyof

  • typeof:获取一个 JS 变量或对象 的 TypeScript 类型。
  • keyof:获取一个 类型 的所有键(keys)组成的字符串联合类型。
typescript
// 1. 定义一个常量对象,存储所有可能的用户类型
export const userTypeMap = {
  singer: '歌手',
  dancer: '舞蹈家',
  writer: '作家',
} as const; // 使用 as const 进行类型收窄,使键值都变为字面量类型

// 2. 使用 typeof 和 keyof 创建联合类型
// typeof userTypeMap -> { readonly singer: "歌手"; readonly dancer: "舞蹈家"; readonly writer: "作家"; }
// keyof typeof userTypeMap -> "singer" | "dancer" | "writer"
type UserType = keyof typeof userTypeMap;

// 3. 在接口定义中使用这个派生出的类型
interface MyInfo {
  name: string;
  type: UserType; // type 只能是 "singer", "dancer", "writer" 三者之一
}

// 使用示例
const info: MyInfo = {
  name: '张三',
  type: 'singer', // ✅ 正确
  // type: 'actor' // ❌ 错误: 不能将类型“"actor"”分配给类型“UserType”
};

收益: 类型定义与常量数据源完全同步。未来如果增加了新的用户类型(如 painter),只需修改 userTypeMap 对象,UserType 类型会自动更新,无需手动修改。

3. 技巧 3:PickOmit 工具类型

场景:有一个复杂的基类型,但某个函数或组件只需要该类型中的部分属性。

问题:如何快速创建一个只包含所需属性(或排除掉不需要属性)的新类型?

解决方案

  • Pick<Type, Keys>:从 Type挑选Keys 联合类型中指定的几个属性,组成一个新类型。
  • Omit<Type, Keys>:从 Type忽略Keys 联合类型中指定的几个属性,剩下的属性组成一个新类型。
typescript
// 基类型:一个完整的用户对象
interface UserProfile {
  id: number;
  username: string;
  email: string;
  avatar: string;
  lastLogin: Date;
}

// 场景A: 我们需要一个只包含 id 和 username 的类型,用于列表显示
// 使用 Pick
type UserPreview = Pick<UserProfile, 'id' | 'username'>;
// 结果: type UserPreview = { id: number; username: string; }

// 场景B: 我们需要一个用于更新表单的类型,它包含除了 id 和 lastLogin 之外的所有属性
// 使用 Omit
type UserUpdatePayload = Omit<UserProfile, 'id' | 'lastLogin'>;
// 结果: type UserUpdatePayload = { username: string; email: string; avatar: string; }


// 函数中使用
function displayUserList(users: UserPreview[]) {
  // ...
}

function updateUser(payload: UserUpdatePayload) {
  // ...
}

收益: 极大地提高了类型的复用性。你可以根据不同场景,从一个基础类型轻松派生出任意的变体,而无需重复声明属性,保持了代码的整洁和一致性。