Closure Compiler是一个JavaScript优化器,它也与TypeScript(使用我们的https://github.com/angular/tsickle作为中间重写器)工作得很好。它产生最小的包,我们在Google内部使用它,并为一些Angular用户提供最小化的应用程序。
在ADVANCED_OPTIMIZATIONS模式下,Closure Compiler重命名非本地属性。它使用一个简单的规则:未加引号的属性访问可以被重命名,但带引号的属性访问不能。这可能会破坏在混合引用和非引用属性访问的情况下的程序,例如一个简单的例子:
window.foo = "hello world";
console.log(window["foo"]);
被Closure Compiler [1] 压缩为
window.a = "hello world";
console.log(window.foo); // prints "undefined"
目前,Closure Compiler将正确引用/非引用访问的责任放在作者身上。这是在这里记录的:https://developers.google.com/closure/compiler/docs/api-tutorial3#propnames
我们相信,通过TypeScript的类型检查器,我们可以标记出大多数导致属性重命名破坏用户程序的情况。我们建议引入一个选项,使带引号和不带引号的属性成为独立的、不匹配的成员。在下面的提案中,假设我们通过选项--strictPropertyNaming
启用了这种行为。
将带引号和不带引号的属性视为不同的
目前,TypeScript允许对命名属性进行带引号的访问(反之亦然):
interface SeemsSafe {
foo?: {};
}
let b: SeemsSafe = {};
b["foo"] = 1; // This access should fail under --strictPropertyNaming
b.foo = 2; // okay
此外,TypeScript 2.2中新引入的问题也存在:
interface HasIndexSig {
[key: string]: boolean;
}
let c: HasIndexSig;
c.foo = true; // This access should fail under --strictPropertyNaming
定义不应重命名其成员的类型的类型
用户可以方便地指定一个类型,以确保属性不会被重命名。例如,当XHR返回时,JSON数据的属性访问必须不会被重命名。
// Under --strictPropertyNaming, the quotes on these properties matter.
// They must be accessed quoted, not unquoted.
interface JSONData {
'username': string;
'phone': number;
}
let data = JSON.parse(xhrResult.text) as JSONData;
console.log(data['phone']); // okay
console.log(data.username); // should be error under --strictPropertyNaming
结构匹配
如果两个类型的引用方式不同,它们不应该有结构匹配:
interface JSONData {
'username': string;
}
class SomeInternalType {
username: string;
}
let data = JSON.parse(xhrResult.text) as JSONData;
let myObj: SomeInternalType = data; // should fail under --strictPropertyNaming
console.log(myObj.username); // would get broken by property renaming
避免混合索引签名和命名属性
可选:我们可以添加一个语义检查 .ts
输入,禁止任何类型同时具有索引签名和命名属性。
interface Unsafe {
[prop: string]: {};
foo?: {}; // This could be a semantic error with --strictPropertyNaming
}
let a: Unsafe = {};
a.foo = 1;
a["foo"] = 2;
请注意,交集运算符 &
仍然会击败这样的检查:
type Unsafe = {
[prop: string]: {};
} & {
foo?: {}; // This is uncheckable because the intersection never fails
}
我们不应该检查 .d.ts
输入,因为它们可能在没有 --strictPropertyNaming
的情况下被编译。
与库的兼容性
如果一个库是用 --strictPropertyNaming
开发的,那么生成的 .d.ts
文件应该可以在任何程序中使用,无论它是否选择启用该标志。然而有一个角落情况需要注意。
以下示例可能应该产生declarationDiagnostic,因为在没有 --strictPropertyNaming
的情况下生成的.d.ts文件在编译时会产生错误。
type C = {
a: number;
'a': string; // this one should probably error
[key: string]: number;
}
一个更简单的选择是允许这种情况发生,并将其生成在 .d.ts
文件中。然后下游消费者只有在打开 --strictPropertyNaming
或 --noLibCheck
时才会收到错误。
在这里,我们对这两种选择都表示满意。
需要引号的属性名
在这种情况下,别无选择,只能给标识符加上引号:
interface JSONData {
'hy-phen': number;
}
这在 --strictPropertyNaming
下仍然有效,但含义是这样的标识符被迫不可重命名,因为没有不带引号的语法来声明它。这似乎没问题,只是为了这样的名称而失去优化,我们认为这种情况很少见。
属性重命名安全性
还有一些情况仍然不安全:
any
类型仍然关闭类型检查,包括检查带引号和不带引号的访问- 在没有
--strictPropertyNaming
开发的库中使用的未加引号的标识符不应被重命名(例如document.getElementById
)。Closure Compiler已经有一个“externs”机制来防止重命名。在TypeScript代码中,这些属性不会被重命名,但现在的情况与今天的情况相同。
5条答案
按热度按时间shyt4zoc1#
这难道不应该是像https://github.com/angular/tsickle这样的工具应该处理的事情吗?
67up9zun2#
为什么不在tsickle中实现呢?
6qftjkof3#
我们将不得不实现自己的类型检查器。目前tsickle只是一个语法导向的树转换。我们很快就会将其移动到emit转换管道API中。想象一下,如果某些emit转换想要以这种方式修改类型系统。
新的transfomration pipeline应该允许tsickle直接插件,无需额外的传递。您可以获得类型检查器状态作为输入,因此无需进行额外的类型检查。
我曾建议tsickle根据类型重写输出。例如,
i.value
=>i["value"]
,如果typeof i
没有对value
的显式声明。这样一来,就不需要在编辑器中报告错误,它应该可以正常工作。06odsfpq4#
正如您在tsickle的链接历史中看到的,@mprobst尝试了双向转换(将类型更改为带有索引签名的引用访问,将类型更改为带有命名属性的未引用访问),并在我们内部的Google进行了推广。
我们遇到了一些问题。首先,这变成了类型导向的发射,而这并不符合TypeScript的工作方式。实际上,它还有很多漏洞(例如,类型仍然可分配,初始赋值未经检查)。
我认为下一步是我、Martin和@rkirov需要更清楚地阐述为什么tsickle重写不起作用。然后我们可以
tpxzln5u5#
关于过去1.5年发生的事情的更新。正如Alex所描述的,我们在angular/tsickle中实现了最小的转换 - 如果用户在
foo
上写了foo.bar
,而该类型为具有索引签名的T
,并且T中没有bar
属性的其他定义,我们将在.js文件中发出foo['bar']
,而不是原始的.ts文件中的foo.bar
。这基本上起作用了,但大约每个月都会有人遇到这个问题。在某些情况下,这种更改(以及通常的Closure优化行为)实际上会破坏原本正常工作的代码。例如:
我们会发出:
Closure会将其更改为(请记住,在Closure中,规则很简单,所有非引号属性都会被更改)
你可能会想知道为什么会出现这种情况。通常,这发生在一个具有许多属性的大型对象上,而用户太懒得再次拼写出来。我建议只使用类型推断,这样可以避免整个问题,但我们无法真正检查每个索引签名的使用情况,以确定它是否合法。
此外,调试此类问题对用户来说也是一场噩梦,因为没有人期望tsickle会改变发出的内容。用户主要查看源.ts和压缩输出,无法通过每个步骤推理。此外,正如Alex所说,从高级别的Angular 来看,非类型导向的.js发出通常是TS的工作方式,每次我们违反这一点都会引起惊讶和困惑。我认为TS团队对于for-of、const枚举等也有类似的经验。
因此,在过去的一个月里,我们将此更改为编译错误而不是tsickle重写 -
https://github.com/bazelbuild/rules_typescript/blob/master/internal/tsetse/rules/property_renaming_safe.ts 和
angular/tsickle@ba13814
我们收到了一些公平的批评,认为我们正在发明一种未经官方批准的自定义TS版本,所以我们仍然希望看到有关--strictPropertyNaming的行动。我们可能会进一步将其降级为tslint,以便在不使用Bazel的仓库中更容易运行此检查。
这里想到的一个风格优势是与我们在实现的最小版本中的
--strictPropertyNaming
相关。当人们看到myObj.myLongProperty
时,他们知道字符串myLongProperty
会被检查拼写错误(除非myObj仍然是any
)。如果没有--strictPropertyNaming
,可以通过索引签名而不检查属性名称的名称来绕过检查,并在拼写错误的情况下暴露出运行时错误。大多数TS作者都知道要限制对any
的使用,但由于不是每个人都意识到它们带来的静态保证损失,因此索引签名仍然存在。