"Function expression, which lacks return-type annotation, implicitly has an 'any' return type" when adding void operation
Asked Answered
S

2

8

I am having a strange issue on TypeScript. I recently learnt about void ... operator because I need to apply it so eslint wouldn't report no-floating-promises. However this particular snippet somehow caused an issue that I cannot reproduce on TypeScript playground:

class A {

  async a() {}

  async onTickAsync(repeat: boolean) {
    try {
      await this.a();
    } catch(e) {
      console.error(e);
    } finally {
      if (repeat) {
        window.setTimeout(() => void this.onTickAsync(true), 200);
      }
    }
  }

}

VS Code would report this error:

enter image description here

TS7011: Function expression, which lacks return-type annotation, implicitly has an 'any' return type.

However, the issue is not reproducible on TS Playground. Both VS Code and Playground is using TypeScript 4.5.4. Here's my tsconfig.json:

{
    "compileOnSave": true,
    "compilerOptions": {
        "noImplicitAny": true,
        "noEmitOnError": true,
        "sourceMap": true,
        "target": "ESNext",
        "module": "ESNext"
    },
    "exclude": [
        "node_modules"
    ]
}

I understand it can be fixed by adding : void return type or removing void operation or removing noImplicitAny:

window.setTimeout((): void => void this.onTickAsync(true), 200);

I want to ask: What causes the error? Why is it happening only on my IDE/local and not the playground?


To be sure it's not just due to VS Code, I ran tsc --version and tsc on a separate Terminal as well:

enter image description here

tsc --showConfig output:

PS C:\Users\lukev\Downloads\Temp> tsc --showConfig
{
    "compilerOptions": {
        "noImplicitAny": true,
        "noEmitOnError": true,
        "sourceMap": true,
        "target": "esnext",
        "module": "esnext"
    },
    "files": [
        "./test.ts"
    ],
    "exclude": [
        "node_modules"
    ],
    "compileOnSave": true
}

It's also interesting that it doesn't happen to other function. This, for example, does not produce any error. It seems to be something with window.setTimeout. I found out there is something different between Function type and () => void for example):

class A {

  doSomething1(_: Function) { }
  doSomething2(_: () => any) { }
  doSomething3(_: () => void) { }

  async a() { }

  async onTickAsync(repeat: boolean) {
    // Only this one produces error
    this.doSomething1(() => void this.onTickAsync(true));
    this.doSomething2(() => void this.onTickAsync(true));
    this.doSomething3(() => void this.onTickAsync(true));
  }

}
Spireme answered 10/1, 2022 at 8:59 Comment(5)
@LukeVo Will you include the output of tsc --showConfig in your question?Granada
@Granada didn't know that command. Added.Spireme
@T.J.Crowder exactly like in your screenshot. 4.5.4 — TypeScript version. Also I think it's not due to VS Code since it happens to independent terminal as well.Spireme
Well that seems really definitive. :-) (I misunderstood your earlier comment about running tsc separately; I get it now.) Thanks!Nainsook
I found out it has something to do with setTimeout using TimerHandler = string | Function as its parameter type. Could be something difference between Playground and my lib.dom.d.ts?Spireme
G
2

See microsoft/TypeScript#36651 for an authoritative answer.

Your issue is that you have enabled the --noImplicitAny compiler option but you have not enabled the --strictNullChecks compiler option. You can set these options in the TypeScript Playground and reproduce your issue.

Aside: note that --strictNullChecks is part of the --strict family of compiler features, which is generally recommended as part of a de facto "standard" level of type safety. You're not really asking about which compiler options you should use, but do note that if you use an uncommon set of compiler options you are more likely to run into compiler behavior that is not well known to the general TypeScript community. Okay, enough about that.

So we know how to reproduce, but haven't explicitly answered why there's an error here. Let's do that now. With --strictNullChecks enabled, the void opterator produces a value of the undefined type. But with --strictNullChecks disabled, there isn't really a distinct undefined type, and the void operator produces a value of the any type. And unless you explicitly annotate that a type is any, you'll get an error under --noImplicitAny:

// with --strictNullChecks disabled

() => void 0; // error!
// Function expression, which lacks return-type annotation, 
// implicitly has an 'any' return type.

(): undefined => void 0; // okay
//^^^^^^^^^^^ <-- arrow function return type annotation

As you saw, you can also get rid of the error if the return type of the void operator is given a contextual type:

function foo(cb: () => any) { }
foo(() => void 0); // okay, void 0 is contextually typed as any

And note that the Function interface is a bit strange and doesn't have a true call signature, see microsoft/TypeScript#20007, so it can't provide a contextual type:

function bar(cb: Function) { }
bar(() => void 0); // implicit any error!

Playground link to code

Gastro answered 10/1, 2022 at 20:34 Comment(2)
Awesome, thank you. The aside note is very helpful actually, I didn't know about that. I will definitely use that flag for my future projects.Spireme
Thanks a lot, this code fixed it: "noImplicitAny": false, "strictNullChecks": false,Geometric
G
0

I can reproduce your issue and, although I can't answer why this is happening, you can fix the compiler error by omitting the void operator, and chaining a catch method to the promise to satisfy ESLint:

// before
window.setTimeout(() => void this.onTickAsync(true), 200);

// after
window.setTimeout(() => this.onTickAsync(true).catch(() => {}), 200);
Granada answered 10/1, 2022 at 9:19 Comment(8)
I am not the one downvoting but I did explain in my question that I know how to fix it. Also sometimes it's actually dangerous to remove it since function returned values could change callback behavior in the future (see description of void operator). In that case, adding explicit : void return type is more favorable :)Spireme
@LukeVo I was just offering another suggestion which you didn't mention that satisfies both the compiler and ESLint.Granada
Ah sorry I didn't notice that. I added an upvote though I think simply adding : void is still simpler (and still have the void operator) and I don't need the catch if I have void operator.Spireme
@LukeVo No worries. My suggestion also actually prevents an unhandled promise rejection (which is what that ESLint rule you are using is trying to help you prevent).Granada
@Granada - The OP's onTickAsync never rejects its promise (unless window.setTimeout throws an error, which I think we can assume it never does).Nainsook
@T.J.Crowder Of course that's right as its written, but ESLint can't determine that (and the function body might change in a way that changes that statement).Granada
@Granada - No, but the OP can, and can decide to use void rather than a pointless catch. The function is clearly written never to reject.Nainsook
Yes, many ways to solve this problem.Granada

© 2022 - 2024 — McMap. All rights reserved.