首页 > 解决方案 > 打字稿方法装饰器有奇怪的行为

问题描述

我有这个代码示例,它说明了我的代码发生了一件奇怪的事情,我想不出为什么会发生这种情况。

基本上,我有一个简单的类 Foo,它只包含一个方法。一个 greet 方法,默认情况下调用时只记录“Hello, World”。但是我有一个装饰器应用于更改方法的方法。新函数几乎相同,但需要一个字符串参数来确定要记录的内容。

这是我的示例的完整代码。

const arr = [];

function methodOverride(target, propertyKey, descriptor) {
    descriptor.value = function(str) {
        console.log("Hello,", str);
    };
    return descriptor;
}


class Foo {
    
    @methodOverride
    greet() {
        console.log("Hello, World");
    }
}

let foo = new Foo();
arr.push(foo);

// foo.greet(); Will result in "Hello, undefined"
// foo.greet("FooBar"); Will result in error.
arr[0].greet("FooBar");

在我的示例中,您可以看到我创建了一个 Foo 实例 foo,并将其推送到一个数组中。我会回到我为什么这样做。但是如果我尝试运行foo.greet()它会记录,当编译的 js 被执行时,Hello, undefined. 它显然期待一个参数,因为该方法已被覆盖。
但是如果我向它传递一个参数foo.greet("FooBar");,Typescript 会抛出一个错误error TS2554: Expected 0 arguments, but got 1.

更奇怪的是,如果我尝试从插入数组的对象中执行该方法,它工作正常。示例中的最后一行代码arr[0].greet("FooBar");将记录,Hello, FooBar

此外,如果我将该对象从数组中取出并将其存储在另一个变量中,称为 bar,那么它传递参数将不会出错。

在我的代码示例下添加它会得到注释中列出的结果。

let bar = arr[0];
bar.greet(); // Hello, undefined
bar.greet("FooBar"); // Hello, FooBar

我希望这些例子足够清楚,任何人都可以遵循。我是 TypeScript 装饰器的新手,我不确定装饰器是否有一些我没有得到的东西,或者问题可能出在 Transpiler 中。

我正在使用全局安装的https://www.npmjs.com/package/typescript将 TS 转换为 JS。我使用的版本是 3.9.7。

这是我用来转换的命令:tsc -p tsconfig.json

这是我的 tsconfig.json 文件。

{      
    "compilerOptions": {
        "outDir": "./",
        "noEmitOnError": true,
        "experimentalDecorators": true,
        "target": "es2020",
        "watch": true,  
    }, 
    "exclude":  []  
}

我认为可能是错误地引发错误的转译的其他原因之一是,如果我设置,noEmitOnError: false那么当编译的 JS 运行时,我会将预期的结果记录到控制台。

任何帮助,将不胜感激。谢谢你。

标签: typescriptdecoratortypescript-decorator

解决方案


你缺少的是装饰器不会改变他们装饰的东西的类型。由于 的greet()方法Foo被声明为 type ()=>void,编译器继续认为它是 type ,()=>void即使你已经装饰了它。装饰不会导致编译器将其更改为(str: string)=>void.

GitHub 中有一个未解决的问题,microsoft/TypeScript#4881,要求支持装饰器类型突变(问题标题谈到类装饰器改变类类型,但该问题也被用于跟踪对方法装饰器改变方法的支持类型)。实现这一点的一个主要绊脚石是TC39 关于向 JavaScript 添加装饰器的提议已经进入第二阶段好几年了。TypeScript 通常不喜欢在第 3 阶段之前承诺实施提案,这是一个特性的一般行为趋于主要固化的地方。不幸的是,TC39 提案与 TypeScript 实施的相比发生了重大变化。因此,在 TC39 提案稳定下来并进入第 3 阶段之前,TypeScript 可能不会对装饰器做任何进一步的事情。

目前,没有我喜欢的解决方法。只需将装饰器用作普通函数,就可以很好地模拟类装饰器突变。但是方法装饰器的变异并不容易以类型安全的方式进行模拟。所以我们可能需要退回到类型断言的等价物,或者更糟的是哄骗编译器接受你正在做的事情。

例如,您可以做的一件事是使用单个重载手动调整调用签名以符合您的预期。如果实现与该调用签名不兼容,您可能还需要在调用签名之前添加//@ts-ignore注释以抑制它。它并不漂亮,但它至少可以让你继续使用你的装饰器并继续生活:

class Foo {
  //@ts-ignore
  greet(str: string): void;
  @methodOverride
  greet(x: number) {
    console.log("Hello, World");
  }
}

let foo = new Foo();
foo.greet("FooBar"); // okay, no error

Playground 代码链接


推荐阅读