TypeScript 实战进阶技巧
一、 第三方库的类型处理技巧
核心目标:准确获取并使用第三方库(如 vue-router, pinia)暴露出的方法、组件或对象的 TypeScript 类型,以增强代码的健壮性和开发效率。
1. 核心技巧:类型定义跳转
在 VS Code 中,按住 Ctrl 键并将鼠标悬停在某个方法或变量上,它会高亮并显示其类型信息。此时单击鼠标左键,可以直接跳转到该类型的定义文件 (.d.ts 文件)。这是探索任何库类型的最快方法。
好处: 无需查阅文档,即可快速了解函数需要什么参数、返回什么类型,以及对象的具体结构。
2. 实战案例 1:vue-router
场景:封装一个动态添加路由的辅助函数 addRouteFn,需要确保传入的路由配置和路由实例类型正确。
问题:如何获取 vue-router 中“单个路由规则”和“路由实例”的类型?
解决方案:
探索类型:通过
Ctrl + Click跳转到createRouter方法的定义,可以发现:- 它接收一个
RouterOptions类型的参数。 - 它返回一个
Router类型的实例(这就是路由实例的类型)。 - 在
RouterOptions中,routes属性的类型是readonly RouteRecordRaw[],因此RouteRecordRaw就是单个路由规则的类型。
- 它接收一个
导入并使用类型:使用
import type语法从库中导入找到的类型。import type是一个性能优化,它确保导入的类型在编译成 JavaScript 后会被完全移除。
// 导入 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 方法。
问题:如何创建一个类型来约束 defineStore 的 setup 函数的返回值?
解决方案:
定义数据和 Store 结构类型:
- 首先,创建
userInfo对象自身的类型接口UserInfoType。 - 接着,创建 Store 的类型
UserInfoStoreType,它描述了setup函数必须返回的对象的结构。 - 要表示响应式数据,需要
Ref类型。通过Ctrl + Click查看ref()函数,可知其返回类型为Ref<T>。
- 首先,创建
编写类型定义:
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;
};- 应用类型约束:在
defineStore时,将这个类型应用到setup函数上。
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 变量中。
问题:如何为 fansList 和 myInfo 的 ref 提供精确的类型,而无需手动复制粘贴 API_Res 中的部分类型定义?
解决方案:使用 Type['key'] 语法直接从父类型中提取指定属性的类型。
// 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:typeof 和 keyof 组合
场景:某个字段(如用户类型 type)的值是固定的几个字符串之一,这些值通常会定义成一个常量对象或枚举以便管理。
问题:如何创建一个代表“这几个固定字符串之一”的联合类型,并且当常量对象更新时,该类型能自动同步?
解决方案:结合使用 typeof 和 keyof。
typeof:获取一个 JS 变量或对象 的 TypeScript 类型。keyof:获取一个 类型 的所有键(keys)组成的字符串联合类型。
// 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:Pick 和 Omit 工具类型
场景:有一个复杂的基类型,但某个函数或组件只需要该类型中的部分属性。
问题:如何快速创建一个只包含所需属性(或排除掉不需要属性)的新类型?
解决方案:
Pick<Type, Keys>:从Type中 挑选 出Keys联合类型中指定的几个属性,组成一个新类型。Omit<Type, Keys>:从Type中 忽略掉Keys联合类型中指定的几个属性,剩下的属性组成一个新类型。
// 基类型:一个完整的用户对象
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) {
// ...
}收益: 极大地提高了类型的复用性。你可以根据不同场景,从一个基础类型轻松派生出任意的变体,而无需重复声明属性,保持了代码的整洁和一致性。