【TypeScript】TypeScript的类型定义与实际业务常见问题代码的对策
@TOC
推荐超级课程:
引言
虽然设计思想会有所不同,但在使用TypeScript进行开发时,编写类型安全的代码被认为是理想的。然而,在实际业务中,由于时间限制、工数限制或多人协作等因素,我们常常会遇到与理想状态相背离的代码。 例如,为了重用现有的JavaScript代码,我们可能会使用any类型,但实际上我们希望定义明确的类型以便在调用时安全使用。或者,由于现有实现复杂且难以理解,我们可能会暂时将其设为nullable,但实际上我们希望彻底调查并安全使用。以下将介绍在TypeScript开发中常见的此类问题及其对策。
(准备知识)类型推断与类型注解
类型推断是指在声明函数时,如果不明确指出函数的类型,TypeScript会根据return的值来推断函数的返回类型(如果没有return,则可能推断为void类型)。
function getUser() = {
return {
name: "Alice",
age: 30
};
};
// 返回值类型为{
// name: string,
// age: number
//}
类型注解则相反,是在声明函数时明确指定类型。在调用函数时,这与类型推断没有区别,但在编辑函数时,如果返回值违反了注解的类型(或者没有返回),则会发生编译错误。
type User = {
name: string;
age: number;
};
function getUser(): User = {
return {
name: "Alice",
age: 30
};
};
//返回值类型为“User”
注解类型可以提高开发时代码的健壮性,减少错误。而类型推断则可以减少描述量,有其优势。
实际业务中的理想与现实
如上所述,在TypeScript开发中,编写类型安全的代码可以减少错误,对开发者来说是理想的代码。但是,在实际业务中,由于各种限制和妥协,我们常常会遇到偏离理想状态的代码。 以下章节将介绍实际业务中常见的问题及其解决对策。
实际业务中常见问题及对策
函数的返回值因类型推断而变成不必要的联合类型
由于类型推断,有时会产生不必要的联合类型返回值。特别是在函数的返回值类型由类型推断决定时,需要注意return的值。以下是一个例子。
function validate(value: string){
if(isSameValue(value)){
return {
validate: false,
message: "同じ名前は使えません"
}
}
return {
validate: true,
message: ""
}
}
在这种情况下,validate的类型如下。
{
validate: boolean;
message: string;
}
如果我们想为“创建模式”添加一个功能,以跳过同名的检查,我们可能会进行如下修改。
function validate(value: string){
// 添加
if(Mode === "Create"){
return {
validate: true
// 本来想包含message: "",但忘记了
}
}
// 以下略
}
这时,validate的类型变成了以下形式。
{
validate: boolean;
message?: undefined;
} | {
validate: boolean;
message: string;
}
最初的对象返回值变成了对象的联合类型。这导致在使用validate函数的返回值时,需要考虑message可能是undefined。 本来我们希望在返回值中包含message,但由于类型推断的状态,在函数内部不会发生编译错误,因此很难注意到这个错误。
对策
通过为函数的类型添加注解,可以防止这种错误。如果时间和工数允许,尽量添加注解是一个好习惯。
// 添加
type ValidateResult = {
validate: boolean;
message: string;
}
function validate(value: string): ValidateResult{ // 添加
// 以下略
}
为简单的函数添加专用的注解可能是费时的,特别是在需要修改现有函数且该函数使用类型推断时,所需的工数可能会更大。然而,如果不添加注解,那么要么需要特别注意返回值类型,要么需要使用单元测试等方法。 简单地说,如果不添加注解,上述问题可能会频繁发生。如果有注解,就可以避免一些不必要的确认,否则就需要编写单元测试来验证。特别是在未来功能可能会增加,或者多人共同开发的情况下,注解的使用是非常有益的。
强行共用Props接口导致信息难以追踪
本章节以React/TypeScript为例,探讨了在实际开发中遇到的问题。即使不了解React,也能理解内容。 在将Props用接口强行共通化时,特别是在多人开发的情况下,可能会导致关系变得非常复杂。特别是当多人协作时,由于工作量和影响范围的限制,常常会倾向于先将属性设置为nullable。 例如,假设有一个ProductSummary组件和一个ProductDetail组件,它们都使用Item接口作为Props。目前,两个组件都实现了使用商品名和商品价格的处理。现在,我们想对ProductDetail进行修改,以显示商品说明的文章。
interface Item {
itemName: string;
itemPrice: number;
itemDescription?: string | undefined // 新功能添加
}
function ProductSummary(props: Item) {
// 使用itemName和itemPrice的处理
}
function ProductDetail(props: Item) {
// 使用itemName, itemPrice和itemDescription的处理 // 新功能添加
}
function MainPage(itemName, itemPrice) {
return (
<ProductSummary props={(itemName, itemPrice)} /> // 在SubPage中也使用,无法轻易将itemDescription设为必填
<ProductDetail props={(itemName, itemPrice, itemDescription)} />
)
}
function SubPage(itemName, itemPrice) {
return <ProductSummary props={(itemName, itemPrice)} />;
}
Item.itemDescription
在ProductSummary中是不必要的,但在ProductDetail中是必需的。由于之前的设计,我们直接在Item接口中添加了nullable的itemDescription。
如果重复这种做法,可能会导致不需要的信息传递给组件,或者由于信息在类型上被设置为nullable,导致调用方忘记传递必要的信息,导致运行时无法传递。
这个问题的一种发展形式是,一个类型被应用于父组件和子组件,使得追踪传递了哪些nullable信息变得非常困难。以下是一个例子。
- ProductSummary有一个子组件ProductDetail,再有一个子组件ProductPreview。
- 所有组件都接收Item类型的Props,但所需的信息各不相同。
interface Item {
itemName: string;
itemPrice: number;
itemDescription?: string | undefined;
itemImage?: Image | undefined; // 新功能添加
}
function ProductSummary(props: Item) {
// 使用itemName和itemPrice的处理
ProductDetail({
itemName: "猪肉",
itemPrice: 100,
itemDescription: "猪肉",
})
}
function ProductDetail(props: Item) {
// 使用itemName, itemPrice和itemDescription的处理
ProductPreview(props)
}
function ProductPreview(props: Item) {
// 使用itemName, itemPrice, itemDescription和itemImage的处理 // 新功能添加
}
在ProductPreview中,我们想要使用Item.itemImage
的信息,但运行时似乎是undefined。由于它是nullable,没有编译错误,所以我们需要在实现中追踪它。
ProductDetail调用了ProductPreview,看起来没有问题。但是在ProductSummary中调用了ProductDetail,那里没有传递itemImage的信息。
如果不遵循这样的步骤,就无法追踪Item.itemImage
的值在哪里发生了变化。在这个例子中,通过跟踪两次调用就可以发现,但在多人开发的大型项目中,这个问题可能会变得混乱,导致难以确定原因。
因此,调查和修正的范围扩大,结果问题往往被搁置。
对策
为了解决这个问题,可以为每个组件准备专用的接口。这样就可以抑制不必要的nullable属性,确保在调用时传递真正必要的信息。
interface Item {
// 省略相同的部分
}
// 新创建
interface ProductDetailItem {
itemName: string;
itemPrice: number;
itemDescription: string;
itemImage: Image;
}
function ProductSummary(props: Item) {
// 使用itemName和itemPrice的处理
ProductDetail({
itemName: "猪肉",
itemPrice: 100,
itemDescription: "猪肉",
// ProductDetailItem.itemImage是必需的,所以这里会出错
})
}
function ProductDetail(props: ProductDetailItem) {
// 使用itemName, itemPrice, itemDescription和item
ProductPreview(props)
}
// 以下略
为ProductDetail准备专用接口后,可以将itemDescription和itemImage设置为non nullable(必填)。这样,如果不传递调用时所需的信息,就会产生错误。 今后,即使新设函数或需要新的属性,也可以采取同样的应对措施,从而降低信息过多或过少的调查成本,使开发变得更加可行。
结语
在业务过程中,从开发者的角度来看,虽然我们希望不惜成本编写高质量的代码,但由于交货期限、工数限制或现有规格的关系,往往不得不做出妥协。这样的情况我认为是非常多的。 本次提出的例子,是笔者在遇到的问题中,特别想要坚守的一些原则。我也希望各位能够参考这篇文章,研究出“适合自己的理想TypeScript编码方式”。 感谢您阅读到最后。