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:
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:
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));
}
}
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!