Typescript:选择类型安全的方法而不是补丁

JavaScript比较著名的特点之一是它的对象和类是“开放”的,即你可以为它们添加任意的属性。这经常会被用来在网页上创建全局变量。通过给windowdocument分配属性:

window.monkey = "Tamarin";
document.monkey = "Howler";

或将数据附加到DOM元素上:

const element = document.getElementById("colobus");
element.home = "tree";

这种风格在jQuery的代码中特别常见。

你甚至可以将属性附加到内置的原型对象上,这样有时会得到令人惊讶的结果:

> RegExp.prototype.monkey = "Capuchin";
"Capuchin"
> /123/.monkey
"Capuchin"

这些通常都不是好的设计。当你把数据附加到window或一个DOM节点上时,你基本上把它变成一个全局对象。这很容易在程序的各个部分之间不经意的引入依赖关系,并意味着在调用函数时必须考虑副作用。

加上TypeScript会引入另一个问题:虽然类型检查器知道DocumentHTMLElement的内置属性,但它不知道你添加的属性:

document.monkey = "Tamarin";
//---类型“Document”上不存在属性"monkey"

修复这个错误最直接的方法是使用any断言:

(document as any).monkey = "Tamarin"; // OK

这能满足类型检查器的要求,但是毫无意外地,它有一些缺点。与任何使用any的情况一样,你会失去类型安全:

(document as any).monky = "Tamarin"; // 也OK, 但是拼写错误
(document as any).monkey = /Tamarin/; // 也OK, 但是类型错误

最好的解决方案是将你的数据从documentDOM中移出。但如果你不能这样做,如你使用的库需要这些数据,或者你正在迁移一个JavaScript应用程序,那么你有几个此号的选择:

一种方法是使用扩增(augmentation),这是interface的特殊能力之一:

interface Document {
 // 猴子补丁的种类
 monkey: string;
}
...
document.monkey = "Tamarin"; // OK

这比使用any有以下几个方面的优势

  • 你可以获得类型安全。类型检查器会标记错误的拼写或错误类型的赋值。
  • 你可以将文档附加到属性上。
  • 你可以在属性上获得自动补全功能。
  • 获得一个有哪些猴子补丁的精确记录。

在模块上下文(即使用import/export)中的TypeScript文件,你需要添加一个declare global来使其工作:

export {};
declare global {
 interface Document {
 // 猴子补丁的种类
 monkey: string;
 }
}
document.monkey = "Tamarin"; // OK

使用扩增的主要问题与作用域有关。

  • 扩增是全局性的。对于代码的其他部分或库来说,你无法将其隐藏起来。
  • 如果该属性的分配发生在应用程序运行过程中,你无法只在这发生之后才引入扩增。当你给HTML元素打补丁时,这一点就特别有问题,因为页面上的一些元素有这个属性,而一些元素没有。出于这个原因,你可能希望将属性声明为string|undefined。这样做更准确,但会使类型不那么方便使用。

另一种方法是使用更准确的类型断言:

interface MonkeyDocument extends Document {
 // 猴子补丁的种类
 monkey: string;
}
(document as MonkeyDocument).monkey = "Tamarin";

对于Typescript来说,这个类型断言是OK的,因为Document和MonkeyDocument是共享属性的,而且你可以在赋值中获得类型安全。作用域问题也更容易管理:Document类型没有全局修改,只是引入了一个新的类型(只在你导入它的范围内)。每当你引用猴子补丁属性时,你必须写一个断言(或者引入一个新的变量)。

作者:指尖上的生活

%s 个评论

要回复文章请先登录注册