如何保持你的TypeScript代码的整洁和整齐?

241 阅读4分钟

我想强调如何保持你的TypeScript代码的整洁和整齐。从本质上讲,这个系列的文章具有很强的观点性,应该用盐(这是复数)来看待。

在TypeScript中,有两种不同的方法来声明对象类型。接口和类型别名。多年来,这两种定义对象类型的方法都被写进了很多博客文章。而随着时间的推移,所有这些都变得过时了。现在,类型别名和接口之间已经没有什么区别了。而且*,*所有不同的东西都已经逐渐统一起来了。

在语法上,它们的区别是细微的。

type PersonAsType = {
  name: string;
  age: number;
  address: string[];
  greet(): string;
};

interface PersonAsInterface {
  name: string;
  age: number;
  address: string[];
  greet(): string;
}

它是一个等号。这种细微差别会对类型评估的时间产生一些影响--对类型别名来说是立即的,对接口来说是懒惰的--但仅此而已。你可以在同样的情况下使用接口和类型别名来做同样的事情。

  • 在类的implements 声明中
  • 作为对象字面的类型注解
  • 用于递归类型结构

你的名字然而,有一个重要的区别,它可能会产生你通常不愿意处理的副作用。

声明合并#

接口允许声明合并,类型别名不允许。声明合并允许向一个接口添加属性,甚至在它被声明之后。

interface Person {
  name: string;
}

interface Person {
  age: number;
}

// Person is now { name: string; age: number; }

TypeScript本身在lib.d.ts 文件中经常使用这种技术,使得它可以根据ECMAScript的版本,直接添加新的JavaScript API的三角符号。如果你想扩展例如Window ,这是一个很好的功能,但在其他情况下,它可能会反击。以此为例。

// Some data we collect in a web form
interface FormData {
  name: string;
  age: number;
  address: string[];
}

// A function that sends this data to a back-end
function send(data: FormData) {
  console.log(data.entries()) // this compiles!! 😱
  // but crashes horrendously in runtime 😕
}

哦,麻烦了,entries() 这个方法是怎么来的?这是一个DOM APIFormData 是浏览器API提供的接口之一,而且有很多。它们是全局可用的,没有什么能阻止你扩展这些接口。而且,如果你这样做,你也不会得到任何通知。

我们当然可以争论正确的命名,但是对于所有你让全局可用的接口来说,问题依然存在,也许是来自于一些依赖关系,你甚至不知道他们在全局空间中添加了这样的接口。

把这个接口改成一个类型别名,可以立即让你意识到这个问题。

type FormData = {
//   ^ 💥 Duplicate identifier 'FormData'.(2300)
  name: string;
  age: number;
  address: string[];
}

它还可以防止你的类型在不知不觉中被扩展。

索引访问类型#

声明合并也是接口不能作为索引访问类型的子集工作的原因。下面是一个向服务器发送数据的例子。你可以传入任何对象和一组HTTP头信息,要求所有键都是string ,所有值都是string

declare function 
  send(data: any, headers: Record<string, string>): void;

Record<string, string> 是与 相同的,这更好地显示了灵活的索引访问。{ [key: string]: string }

让我们为所需的HTTP头文件做两个类型的定义。一次是作为对象类型。

type HTTPHeaders = {
  Accept: string,
  Cookie: string
}

而另一个是作为一个接口。

interface HTTPHeaderInterface {
  Accept: string,
  Cookie: string,
}

如果你用一个已经被注释为HTTPHeaders 的对象来调用send ,一切都很美好。

const hdrs: HTTPHeaders = {
  Accept: "text/html",
  Cookie: ""
};

send({}, hdrs) // 👍

但是当你把hdrs 改为HTTPHeadersInterface 的时候,事情就会变得很糟糕。

const hdrs: HTTPHeaderInterface = {
  Accept: "text/html",
  Cookie: ""
};

send({}, hdrs) 
//       ^ 💥 Index signature is missing in type 'HTTPHeaderInterface'

TypeScript会抱怨索引签名丢失。只有当类型是最终的,像HTTPHeaders ,TypeScript才能正确地检查所有的属性和值是否可以分配给我们在send 中声明的Record<string, string> 类型。由于接口是为声明合并准备的,因此不是所有的属性都是已知的,TypeScript无法判断索引签名是否与Record<string, string>

这就是为什么我建议首选类型别名而不是接口。当然,如果你提供的库中的接口应该可以被其他人扩展,那么类型别名不会让你走得太远。但除此之外,类型别名是清晰、简单和整洁的。