2

I have this sample of code that illustrates a weird thing happening with my code, and I can't figure a reason why this would happen.

Basically, I have a simple class, Foo, that contains only one method. A greet method, that by default when called simply logs, "Hello, World". But I have a decorator applied to the method that changes the method. The new function is almost the same, but takes a string argument to determine what to log.

Here is the full code of my example.

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");

In my example, you can see I create a Foo instance, foo, and push it into an array. I'll get back to why I am doing that. But If I try to run foo.greet() it will log, when the compiled js is executed, Hello, undefined. It's clearly expecting an argument because the method was overridden.
But if I pass an argument to it, foo.greet("FooBar");, Typescript will throw an error error TS2554: Expected 0 arguments, but got 1.

The even stranger thing is if I try to execute the method from the object that was inserted into the array, it works fine. The last line of code in the example, arr[0].greet("FooBar"); will log, Hello, FooBar

Also, if I take that object out of the array and store it in a another variable, call it bar, then it passing the argument will work with no erorrs.

Adding this under my code example will get the results listed in the comment.

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

I hope these examples are clear enough for anyone to follow. I am new to TypeScript decorators, and I'm not sure if there is something about decorators that I am just not getting, or perhaps the problem lies within the Transpiler.

I am using https://www.npmjs.com/package/typescript installed globally to transpile TS to JS. The version I am using is 3.9.7.

Here is the command I am using to transpile: tsc -p tsconfig.json

And here is my tsconfig.json file.

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

One of the other reasons I think it might be the transpiling that is erroneously throwing the error is that if I set noEmitOnError: false then when the compiled JS runs I will have the expected results logged to the console.

Any help would be appreciated. Thank you.

1 Answer 1

1

What you're missing is that decorators do not mutate the type of the things they decorate. Since the greet() method of Foo is declared to be of type ()=>void, the compiler continues to think it is of type ()=>void even after you have decorated it. The decoration does not cause the compiler to mutate it to (str: string)=>void.

There is an open issue in GitHub, microsoft/TypeScript#4881, asking for support for decorator type mutation (the issue title talks about class decorators mutating the class type, but the issue is also being used to track support for method decorators mutating the method type). A major stumbling block in getting this implemented is that the TC39 proposal for adding decorators to JavaScript has been in Stage 2 for several years now. TypeScript generally doesn't like to commit to implementing proposals before Stage 3, which is where the general behavior of a feature tends to be mostly solidified. Unfortunately, the TC39 proposal is changing significantly from what TypeScript has implemented. And therefore TypeScript is probably not going to do anything further with decorators until the TC39 proposal settles down and advances to Stage 3. TypeScript now supports JS Decorators, but the type situation remains unchanged: decorators still do not mutate types.

For now, there's not a workaround I like much. Class decorator mutation can be simulated soundly just by using the decorator as a plain function. But method decorator mutation is not as easily simulated in a type safe way. So we might need to fall back to the equivalent of type assertions or worse to cajole the compiler into accepting what you're doing.

For example, one thing you could do is to use a single overload to manually tweak the call signature to correspond to what you expect it to be. If the implementation is not compatible with that call signature, you might also need to add a //@ts-ignore comment before the call signature to suppress it. It's not pretty, but it at least lets you continue using your decorator and move forward with life:

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 link to code

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for the response, it's helpful. I guess one of my main problems is that I don't understand why the decorated function works with the string argument once I pushed it into an array.
I can't reproduce your issue with the array. If you can show me that happening in the TypeScript Playground or a web IDE I might be able to answer definitively. Without a reproduction, I could guess... maybe arr is inferred as type any[] and then arr[0] is any and arr[0].glorp(1234, "oops") would not give a compiler warning? Which is one reason to stay away from any.
I tried executing the same code in a TypeScript playground and there I got two errors, where in my own development I am running the same code receiving only 1 error, same ts versions. Given all the information I have, I think I'll just assume that the problem is the TS transpiler not catching an error that it should be catching.
I think you can find the code I am running here: typescriptlang.org/play?ts=3.9.7#code/…

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.