Legacy Decorator with Computed Property

JavaScript

下面这段 TypeScript / JavaScript 代码,如果使用 Babel 编译,会报错(可以在这里尝试一下):

const name = 'key';
class Something {
  @decorator [name]() { return 'value' };
}

报错内容为:Unexpected token。但如果使用 tsc,就可以正常编译。

事实上,Babel 对 TypeScript 的处理基本上就是简单的把类型定义部分给删掉,变成一段普通的 JavaScript 代码,然后再通过 Babel 转译成指定的版本。因此,本质上还是 Babel 在处理 JavaScript 的 decorator 的时候,出现了问题。

需要事先说明的是,上面这段代码在 Babel 转译的时候需要使用到 babel-plugin-proposal-decorators 中的 legacy 模式;对应 TypeScript 则是开启 compilerOption 中的 experimentalDecorators。这里使用的 decorator 语法并不是当下 stage-2 的提案版本,而是之前版本的 stage-2 提案。两者其实有着非常显著的区别:被废弃的提案更灵活,功能也更加的丰富,但灵活性/动态性也让静态代码分析变得很困难;新版本功能更受限制,但也让静态分析变得更容易了(相关的说明可以参考提案中的解释)。

回到上面的报错,这里之所以 Babel 会给出语法错误的提示,也正是因为老版本 decorator 的动态性。

具体来说,当 decorator 和 [] 一起被使用的时候,其实有两种可能性:

  1. 开发者是希望将 decorator 应用到一个计算属性上:这里的 [name] 是一个计算属性值,比如上面的代码就等价于 @decorator key = 'value'
  2. 开发者是希望将 decorator 这个对象中的 name 属性给取出来,作为真正的 decorator 来使用:也就是说,@decorator [name] 其实等价于 @decorator[name]

显然,Babel 在处理的时候,选择了第二种解释的方案,而 TypeScript 选择了第一种。正因为如此,由于 @decorator[name] 这个 decorator 的后面缺少了被装饰的属性名称,Babel 就报错了。

注:在 JavaScript 中,取下标的时候是可以添加空格的,JavaScript 会忽略这里的空格。比如,下面的取值语句并没有问题:

const map = { key: 'value' };
console.log(map ['key']); // works!

而如果将 babel-plugin-proposal-decorators 改为非 legacy 模式,上述的编译就不会报错了。这是因为,根据最新的提案,@decorator 是被定义为 decorator 的,因此不存在还需要从 decorator 中取 name 属性来作为 decorator 的情况。

更多关于新提案的细节,可以参考这里

关于这个问题本身,之前在 GitHub 上有提出 issue 作为讨论,可以在这里找到。

另外,tsc 编译不会出错是因为 TypeScript 不允许类似 @obj[key] 这样的写法。下面是一段在 TypeScript 中会报错,但是在 Babel 中不会报错的代码:

function enumerable(target: any, prop: string, descriptor: PropertyDescriptor) {
  descriptor.enumerable = true;
}

const obj = { enumerable };

class A {
  @enumerable
  works() { }

  @(obj[enumerable])
  error() {  }
}

可以在这里尝试 TypeScript 编译,在这里尝试 Babel 编译。