从closuretools.blogspot.co.uk/2012/02/type-checking-tips.html逐字复制,因为“谢谢你的建议。太糟糕的blogspot在中国被封锁了,所以无法阅读。我看到了很多有关此的文章在 blogspot 上但看不到”
Closure Compiler 的类型语言有点复杂。它具有联合(“变量 x 可以是 A 或 B”)、结构函数(“变量 x 是返回数字的函数”)和记录类型(“变量 x 是具有属性 foo 和 bar 的任何对象”)。
很多人告诉我们,这仍然不够表达。有很多方法可以编写不完全适合我们的类型系统的 JavaScript。人们建议我们应该添加混入、特征和匿名对象的事后命名。
这对我们来说并不特别令人惊讶。JavaScript 中的对象规则有点像 Calvinball 的规则。您可以随时更改任何内容,并制定新规则。很多人认为一个好的类型系统可以给你一种强有力的方式来描述你的程序的结构。但它也给了你一套规则。类型系统确保每个人都同意什么是“类”、什么是“接口”以及“是”的含义。当您尝试向无类型的 JS 添加类型注释时,您不可避免地会遇到我头脑中的规则与您头脑中的规则不太匹配的问题。没关系。
但令我们惊讶的是,当我们给人们这种类型系统时,他们经常会找到多种方式来表达同一件事。有些方法比其他方法效果更好。我想我会写这篇文章来描述人们尝试过的一些事情,以及他们是如何解决的。
函数与函数()
有两种方式来描述一个函数。一种是使用 {Function} 类型,编译器将其字面解释为“任何对象 x 其中 'x instanceof Function' 为真”。A{Function}
是故意糊状的。它可以接受任何参数,并返回任何东西。您甚至可以在其上使用“新”。编译器将允许您随意调用它而不会发出警告。
结构函数更加具体,让您可以细粒度地控制函数的功能。A{function()}
没有参数,但我们不在乎它返回什么。A{function(?): number}
返回一个数字并只接受一个参数,但我们不关心该参数的类型。当你用“new”调用它时, A{function(new:Array)}
会创建一个数组。我们的类型文档和 JavaScript 样式指南有更多关于如何使用结构函数的示例。
很多人问我们是否{Function}
气馁,因为它不太具体。实际上,它非常有用。例如,考虑 的定义Function.prototype.bind
。它可以让你对函数进行柯里化:你可以给它一个函数和一个参数列表,它会给你一个带有“预填充”参数的新函数。我们的类型系统不可能表示返回的函数类型是第一个参数类型的转换。所以 JSDoc onFunction.prototype.bind
说它返回 a {Function}
,编译器必须有手工编码的逻辑才能找出真正的类型。
也有很多情况下你想传递一个回调函数来收集结果,但结果是特定于上下文的。
rpc.get(‘MyObject’, function(x) {
// process MyObject
});
如果你传递的回调参数必须对它得到的任何东西进行类型转换,那么“rpc.get”方法就更加笨拙了。所以通常更容易给参数一个 {Function} 类型,并相信调用者类型不值得类型检查。
对象与匿名对象
许多 JS 库定义了一个包含许多方法的全局对象。该对象应该有什么类型的注释?
var bucket = {};
/** @param {number} stuff */ bucket.fill = function(stuff) {};
如果你来自 Java,你可能会想给它类型 {Object}。
/** @type {Object} */ var bucket = {};
/** @param {number} stuff */ bucket.fill = function(stuff) {};
这通常不是你想要的。如果你添加一个“@type {Object}”注解,你不仅仅是告诉编译器“bucket is an Object”。你告诉它“桶可以是任何对象”。所以编译器必须假设任何人都可以将任何对象分配给“桶”,并且程序仍然是类型安全的。
相反,您通常最好使用@const。
/** @const */ var bucket = {};
/** @param {number} stuff */ bucket.fill = function(stuff) {};
现在我们知道桶不能分配给任何其他对象,并且编译器的类型推理引擎可以对桶及其方法进行更强大的检查。
一切都可以只是记录类型吗?
JavaScript 的类型系统并没有那么复杂。它有 8 种具有特殊语法的类型:null、undefined、boolean、number、string、Object、Array 和 Function。有些人注意到记录类型允许您定义“具有属性 x、y 和 z 的对象”,而 typedef 允许您为任何类型表达式命名。所以在两者之间,您应该能够使用记录类型和 typedef 定义任何用户定义的类型。这就是我们所需要的吗?
当您需要一个函数来接受大量可选参数时,记录类型非常有用。所以如果你有这个功能:
/**
* @param {boolean=} withKetchup
* @param {boolean=} withLettuce
* @param {boolean=} withOnions
*/
function makeBurger(withKetchup, withLettuce, withOnions) {}
你可以让它更容易像这样调用:
/**
* @param {{withKetchup: (boolean|undefined),
withLettuce: (boolean|undefined),
withOnions: (boolean|undefined)}=} options
*/
function makeBurger(options) {}
这很好用。但是当你在一个程序的很多地方使用相同的记录类型时,事情会变得有点麻烦。假设您为 makeBurger 的参数创建了一个类型:
/** @typedef {{withKetchup: (boolean|undefined),
withLettuce: (boolean|undefined),
withOnions: (boolean|undefined)}=} */
var BurgerToppings;
/** @const */
var bobsBurgerToppings = {withKetchup: true};
function makeBurgerForBob() {
return makeBurger(bobsBurgerToppings);
}
后来,Alice 在 Bob 的库之上构建了一个餐厅应用程序。在一个单独的文件中,她尝试添加洋葱,但搞砸了 API。
bobsBurgerToppings.withOnions = 3;
Closure Compiler 会注意到 bobsBurgerToppings 不再匹配 BurgerToppings 记录类型。但它不会抱怨 Alice 的代码。它会抱怨 Bob 的代码出现类型错误。对于非平凡的程序,Bob 可能很难弄清楚为什么类型不再匹配。
一个好的类型系统不仅仅表达关于类型的契约。它还为我们提供了一种在代码违反合同时分配责任的好方法。因为一个类通常定义在一个地方,编译器可以找出是谁破坏了类的定义。但是,当您有一个匿名对象被传递给许多不同的函数,并且具有从许多不同的地方设置的属性时,对于人类和编译器来说,要找出谁在破坏类型契约要困难得多。
作者:软件工程师 Nick Santos