从项目复查做一些TypeScript使用上的总结
前言
针对年终项目代码复查,团队开发中存在一些代码一些问题的总结,在对TypeScript使用上存在很多偏差,代码存在大量any类型,特此做一个整体的使用总结。
ts的接口继承
问题:发现现有很多代码存在大量重复,简单来说就是ts类型没有复用和继承。
在实际开发中,一般我们对于每个接口都要定义好请求参数的类型和返回参数的类型。而对于一些比较常见的功能,我们一般会有固定的参数和一些固定的返回字段。
例如列表接口,会有page和size,这个时候我们就可以先写一个基础的列表请求类型如:
interface ReqPage {
page?: number;
size?: number;
}
这样在后续其他列表的请求参数,我们在定义的时候就可以继承这个ReqPage去复用。假设我们现在有一个UserList接口,我们定义请求参数的类型就可以这样写:
interface UserListParams extends ReqPage {
name: string;
id: string | number;
}
在开发中,直接继承可以避免重复定义。
ts的类型别名
对于以上也可以用type(类型别名)定义:
类型别名可以组合多个类型,用 &(交叉符)连接:
type ReqPage = { current?: number; size?: number };
type UserListParams = {
name: string;
id: string | number;
} & ReqPage
// 交叉类型适用于添加通用属性(如时间戳、审核状态)到现有类型。
// 例如:
type User = {
id: number;
name: string;
};
type Timestamps = {
createdAt: Date;
updatedAt: Date;
};
type UserWithTimestamps = User & Timestamps;
const user: UserWithTimestamps = {
id: 1,
name: "Alice",
createdAt: new Date(),
updatedAt: new Date(),
};
类型别名可以表示多个可能的类型,用 |(管道符)分隔,举个场景,使用联合类型处理枚举式值,例如API 响应状态:
type ResponseType = "success" | "error";
function handleResponse(response: ResponseType) {
if (response === "success") {
console.log("Operation successful");
} else {
console.log("Operation failed");
}
}
类型别名可以表示复杂的嵌套类型,尤其在定义嵌套的对象或数组时非常有用
type ApiResponse<T> = {
data: T;
status: number;
error?: string;
};
type User = {
id: number;
name: string;
};
type UserResponse = ApiResponse<User>;
const response: UserResponse = {
data: { id: 1, name: "Bob" },
status: 200,
};
// ApiResponse 是一个通用模板类型,可以复用在不同的数据模型中。
类型别名 vs 接口
相似点
都可以描述对象类型。
都可以在函数参数中使用。
不同点
扩展性:
接口:可以继承和合并。
类型别名:不能直接合并,但可以通过交叉类型(&)实现类似功能。
灵活性:
类型别名:可以表示 对象类型、基本类型、联合类型、交叉类型、元组、条件类型 等几乎任何类型。
接口:只能表示对象类型和函数类型。
一些类型别名的其他用法
// 函数类型
// 1、实际应用:定义回调函数或通用的函数签名。
type GreetFunction = (name: string) => string;
const greet: GreetFunction = (name) => `Hello, ${name}!`;
console.log(greet("Alice")); // 输出: Hello, Alice!
// 2、表示固定长度的数组,如二维坐标点或 RGB 值。类型别名支持元组类型。
type Point = [number, number];
const origin: Point = [0, 0];
const destination: Point = [10, 15];
// 3、处理数组或动态数据类型。条件类型配合类型别名可以实现类型的动态生成
type ElementType<T> = T extends Array<infer U> ? U : T;
type StringArray = string[];
type StringElement = ElementType<StringArray>; // string
type NumberElement = ElementType<number>; // number
类型别名的实际使用:参考
// 1、动态定义类型
type FormField = {
name: string;
value: string;
};
type Form<T extends FormField> = {
fields: T[];
};
type LoginField = FormField & { required: boolean };
type LoginForm = Form<LoginField>;
const loginForm: LoginForm = {
fields: [
{ name: "username", value: "", required: true },
{ name: "password", value: "", required: true },
],
};
// 2、API 请求封装:封装通用的请求和响应类型。
type RequestParams = {
url: string;
method: "GET" | "POST";
body?: Record<string, any>;
};
type ApiResponse<T> = {
data: T;
error?: string;
};
function fetchApi<T>(params: RequestParams): Promise<ApiResponse<T>> {
// 模拟请求
return new Promise((resolve) => {
resolve({ data: {} as T, error: undefined });
});
}
// 使用封装
type User = { id: number; name: string };
fetchApi<User>({ url: "/user", method: "GET" }).then((res) => {
console.log(res.data.id); // 用户 ID
});
ts的工具类型
问题:不会使用工具类型:发现项目代码中TypeScipt的工具类型如Partial、Pick、Exclude、Omit等几乎没有。
举个在日常工作中经常遇到的例子,还是后台管理系统中常见的列表功能。一般列表上面会存在一些搜索选项,如名称搜索、id搜索等,这个时候我们就可以用上我们的工具类型了
1、Partial:将类型中的所有属性变为可选属性(optional)
// 假如我们现在有一个用户列表,他的返回格式如下:
type User = {
id: number;
name: string;
email: string;
};
// 现在name id age grade都可以作为搜索条件,那我们就可以使用Partial
type PartialUser = Partial<User>;
const user1: PartialUser = {
name: "Alice",
}; // 合法,因为所有属性都是可选的
2、Required:将类型中的所有属性变为必填(required)。
3、Readonly:将类型中的所有属性变为只读(readonly)。
4、Pick:从类型中选择指定的属性,构造一个新类型。使用较高
以下是实际应用中pick可以存在的场景
// API 响应的字段选择、表单字段验证、角色权限管理
// 1 API 响应中的字段选择
type ApiResponse = {
id: number;
name: string;
email: string;
address: string;
phone: string;
};
// 创建一个只包含用户的 ID 和名字的类型
type UserPreview = Pick<ApiResponse, "id" | "name">;
const response: UserPreview = {
id: 1,
name: "Bob"
};
// 2 表单字段类型选择
// 假设我们有一个 FormData 类型,它包含了多个表单字段。在表单的提交中,某些表单字段在不同的情况(如编辑模式或添加模式)下是可选的。在这种情况下,Pick 工具类型可以用来选择表单的必要字段。
type FormData = {
firstName: string;
lastName: string;
email: string;
password: string;
confirmPassword: string;
};
// 创建一个类型,只包含必填字段
type RequiredFields = Pick<FormData, "firstName" | "lastName" | "email">;
const formData: RequiredFields = {
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com"
};
// 3 从接口中选择字段
// 在设计复杂的系统时,可能会涉及到多种角色(如管理员、用户、客户等),他们在系统中的角色权限和字段会有所不同。Pick 可以帮助我们从大型接口中提取不同角色需要的字段。
interface User {
id: number;
name: string;
email: string;
role: string;
lastLogin: Date;
}
type AdminUser = Pick<User, "id" | "name" | "role">;
type RegularUser = Pick<User, "id" | "name" | "email">;
const admin: AdminUser = {
id: 1,
name: "Alice",
role: "Admin"
};
const regularUser: RegularUser = {
id: 2,
name: "Bob",
email: "bob@example.com"
};
// 4 从接口中选择不同的查询字段
// 在进行数据查询时,API 返回的数据往往包含很多字段,有些字段在某些查询条件下可能并不需要。通过 Pick 可以选择你想要的字段,简化代码。
interface Product {
id: number;
name: string;
price: number;
description: string;
stock: number;
}
// 从 `Product` 中选择 `id`, `name` 和 `price` 字段用于展示
type ProductPreview = Pick<Product, "id" | "name" | "price">;
const preview: ProductPreview = {
id: 101,
name: "Laptop",
price: 999.99
};
5、Omit:从类型中排除指定的属性,构造一个新类型
// 用于去除敏感数据(如密码)或冗余字段。
type User = {
id: number;
name: string;
email: string;
};
type OmittedUser = Omit<User, "email">;
const user5: OmittedUser = {
id: 1,
name: "Eve",
};
6、Exclude:从联合类型中排除指定的类型。
// 过滤掉一些不需要的类型
type Union = "a" | "b" | "c";
type Excluded = Exclude<Union, "b">; // "a" | "c"
7、Extract:从联合类型中提取指定的类型。
// 提取需要的子类型。
type Union = "a" | "b" | "c";
type Extracted = Extract<Union, "b" | "c">; // "b" | "c"
其他工具类如
Record、NonNullable、ReturnType、Parameters、ConstructorParameters 和 InstanceType等不再做详述
个人认为使用工具类有两个好处:
将两个有关联的类型关联起来,而不是凭空创造一个新的类型。让团队中的其他人知道她们是属于一个功能模块的。
增加复用性,而不是单纯的copy yourself
泛型(Generics)
问题:不会使用泛型或者会使用但图方便不去用
泛型允许定义可复用的组件或函数,适用于处理多种数据类型的场景。
// 例如列表数据返回:data-当前页数的数据, total-总数, size-页数等
interface ResPage<T> {
data: T[];
current: number;
size: number;
total: number;
}
const userResponse: ResPage<{ id: number; name: string }> = {
current: 1;
size: 2;
total: 12;
data: { id: 1, name: "Alice" },
};
// T 是泛型占位符 具体类型在调用时推断出来。
泛型接口
// 泛型接口用于定义可以接受多种类型的接口,适合于需要支持不同类型但又具有相同结构的场景。
interface ApiResponse<T> {
status: number;
data: T;
}
const userResponse: ApiResponse<{ id: number; name: string }> = {
status: 200,
data: { id: 1, name: "Alice" },
};
const productResponse: ApiResponse<{ id: number; price: number }> = {
status: 200,
data: { id: 1, price: 100 },
};
// API 请求封装:你可以使用泛型接口来封装多个 API 请求类型,避免重复编写不同的请求响应类型。
interface User {
id: number;
name: string;
}
interface Product {
id: number;
title: string;
price: number;
}
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
const data = await response.json();
return data as T;
}
// 使用泛型请求不同类型的数据
fetchData<User>("https://api.example.com/user").then((user) => console.log(user.name));
fetchData<Product>("https://api.example.com/product").then((product) => console.log(product.title));
AnyScript
问题:图方便乱用any
例如:代码中大量存在any的使用
const onDataChange = (value: any) => {
emit("updataData", value);
};
let Code: any = "";
我们应该在每次想使用any之前多问自己一下,这个类型真的无法定义吗?
枚举(enum)和字面量类型(Literal Types)
问题:不会使用枚举和字面量类型
什么是枚举(enum)?
枚举是一种 TypeScript 提供的特殊类型,用于定义一组常量。枚举的主要作用是给一组相关常量(数字或字符串)提供一个更加具有描述性的名字,从而避免直接使用数字或字符串值。
// 状态管理:在复杂的应用程序中,使用枚举来表示不同的状态,比如订单状态(待付款、已发货、已完成等)、任务状态(未开始、进行中、已完成等)等。
enum Status {
Pending, // 默认值是 0
InProgress, // 默认值是 1
Completed // 默认值是 2
}
interface Task {
title: string;
status: Status;
}
const task: Task = {
title: "Build the app",
status: Status.InProgress // 这是有效的
};
const invalidTask: Task = {
title: "Test the app",
status: 4 // Error: Type '4' is not assignable to type 'Status'
};
// 枚举能够帮助我们确保变量值的合法性,使得代码更加类型安全。使用枚举时,status 属性只能是 Status.Pending、Status.InProgress 或 Status.Completed 之一,而不能是其他任意值。
什么是字面量类型?
字面量类型指的是指定某个特定值的类型。
例如,你可以定义一个类型,使其值只能是特定的字符串、数字或布尔值。与枚举不同,字面量类型更加简洁,适用于值是固定且有限的场景。
字面量类型通常是通过联合类型(|)来定义的,表示某个值可以是多个指定值中的一种。
type Status = "Pending" | "InProgress" | "Completed";
const task = {
title: "Build the app",
status: "InProgress" // 这是有效的
};
const invalidTask = {
title: "Test the app",
status: "Finished" // Error: Type '"Finished"' is not assignable to type 'Status'
};
// 在这个例子中,Status 字面量类型仅允许值为 "Pending"、"InProgress" 或 "Completed" 之一。
框架或组件库使用参考与提升
vue3里面的ref,computed是可以定义类型
// ref
const userName = ref<string>("");
// computed
const userName = computed<string>(() => {
return "haha";
});
同样的对element-plus,使用的时候可以多看下文档,看下怎么定义类型:
对于一些第三方库文档上没有的类型定义,可以自己翻查一下node_modules/types或者对应第三方库的d.ts文件。
其实t本质上与js没有区别,我们要把它当作是一个工具,去帮助我们提升代码的健壮性,不要产生抵触情绪,同时学习下官方文档对应的类型定义对于自己的水平也会大有提升。