TypeScript 建议:引用和未引用的属性名称是不同的,

brvekthn  于 4个月前  发布在  TypeScript
关注(0)|答案(5)|浏览(42)

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代码中,这些属性不会被重命名,但现在的情况与今天的情况相同。
shyt4zoc

shyt4zoc1#

这难道不应该是像https://github.com/angular/tsickle这样的工具应该处理的事情吗?

67up9zun

67up9zun2#

为什么不在tsickle中实现呢?

  • 我们必须自己实现类型检查器。目前,tsickle只是一个语法导向的树转换工具。我们很快就会将其移动到emit转换管道API中。想象一下,如果某个emit转换想要以这种方式修改类型系统。
  • 在emit过程中再次进行类型检查会很慢
  • 我们需要编辑器工具提示建议正确的补全、错误高亮等
6qftjkof

6qftjkof3#

我们将不得不实现自己的类型检查器。目前tsickle只是一个语法导向的树转换。我们很快就会将其移动到emit转换管道API中。想象一下,如果某些emit转换想要以这种方式修改类型系统。

  • 在emit期间再次进行类型检查程序会很慢

新的transfomration pipeline应该允许tsickle直接插件,无需额外的传递。您可以获得类型检查器状态作为输入,因此无需进行额外的类型检查。

  • 我们希望编辑器工具能够建议正确的补全、错误高亮等

我曾建议tsickle根据类型重写输出。例如,i.value => i["value"],如果typeof i没有对value的显式声明。这样一来,就不需要在编辑器中报告错误,它应该可以正常工作。

06odsfpq

06odsfpq4#

正如您在tsickle的链接历史中看到的,@mprobst尝试了双向转换(将类型更改为带有索引签名的引用访问,将类型更改为带有命名属性的未引用访问),并在我们内部的Google进行了推广。
我们遇到了一些问题。首先,这变成了类型导向的发射,而这并不符合TypeScript的工作方式。实际上,它还有很多漏洞(例如,类型仍然可分配,初始赋值未经检查)。
我认为下一步是我、Martin和@rkirov需要更清楚地阐述为什么tsickle重写不起作用。然后我们可以

  • 重新审视这个问题,以便我们有严谨性和正确性
  • 尝试另一个实验,当我认为引用可能是错误的时,让tsickle(或tslint)发出警告
tpxzln5u

tpxzln5u5#

关于过去1.5年发生的事情的更新。正如Alex所描述的,我们在angular/tsickle中实现了最小的转换 - 如果用户在foo上写了foo.bar,而该类型为具有索引签名的T,并且T中没有bar属性的其他定义,我们将在.js文件中发出foo['bar'],而不是原始的.ts文件中的foo.bar
这基本上起作用了,但大约每个月都会有人遇到这个问题。在某些情况下,这种更改(以及通常的Closure优化行为)实际上会破坏原本正常工作的代码。例如:

let x: {[key: string]: string} = {someProperty: 'foo'};
x.someProperty;

我们会发出:

let x = {someProperty: 'foo'};
x['someProperty'];

Closure会将其更改为(请记住,在Closure中,规则很简单,所有非引号属性都会被更改)

let x = {a: 'foo'};
x['someProperty'];

你可能会想知道为什么会出现这种情况。通常,这发生在一个具有许多属性的大型对象上,而用户太懒得再次拼写出来。我建议只使用类型推断,这样可以避免整个问题,但我们无法真正检查每个索引签名的使用情况,以确定它是否合法。
此外,调试此类问题对用户来说也是一场噩梦,因为没有人期望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的使用,但由于不是每个人都意识到它们带来的静态保证损失,因此索引签名仍然存在。

相关问题