-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
feat: allow global type declaration in no-var
#19714
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
When declaring a global variable in TypeScript, using `var` vs `let`/`const` yields different behaviour. A declaration using `var` yields the expected result. See [TypeScript playground](https://www.typescriptlang.org/play/?#code/PQgEB4CcFMDNpgOwMbVAGwJYCMC8AiAEwHsBbfUYAPgFgAoew6ZdAQxlAHN1jtX1QAb3qhRGaABdQAGUkAuUAGcJkTIk4ixAN3agAauwXLV6+ptFqJCWK1SgA6mpIB3IebGjHiIyrUa6HgC+9MEMdGDcvPygtqiKivSyEvQGkPReZuHAoM5OxK6ckvS5iC4AdEnFec5lqVWl+WUZYWAlLkpFdG2NSaC4oADkA-XlqX2Dw13VTWrjQ5kRPHzoACoAFpiKXJ2Ry+ubFTtL-PuKtez0uycbZ830i1GrNx3JdFdPB73982-HH2djb6Td6nGaIOaTe7ZRTQdCwbavGFww6I2Gwc5pOhI9F3LIdOEvejYlEQolojGkrHkryU+jQAAeAAdiJApIJAkA) Refs eslint#19173 Refs microsoft/vscode#248075 Refs typescript-eslint/typescript-eslint#2594 Refs typescript-eslint/typescript-eslint#7941
✅ Deploy Preview for docs-eslint ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for picking this up, few comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also add a section in no-var
docs with correct
, incorrect
examples that says it also support Typescript syntax?
for reference https://eslint.org/docs/latest/rules/no-invalid-this
Good catch! Done |
@@ -54,6 +54,42 @@ const CONFIG = {}; | |||
|
|||
::: | |||
|
|||
This rule additionally supports TypeScript type syntax. There are multiple ways to declare global variables in TypeScript. Only using `var` works for all cases. See this [TypeScript playground](https://www.typescriptlang.org/play/?#code/PQgEB4CcFMDNpgOwMbVAGwJYCMC8AiAEwHsBbfUYAPgFgAoew6ZdAQxlAHN1jtX1QAb3qhRGaABdQAGUkAuUAGcJkTIk4ixAN3agAauwXLV6+ptFqJCWK1SgA6mpIB3IebGjHiIyrUa6HgC+9MEMdGDcvPygtqiKivSyEvQGkPReZuHAoM5OxK6ckvS5iC4AdEnFec5lqVWl+WUZYWAlLkpFdG2NSaC4oADkA-XlqX2Dw13VTWrjQ5kRPHzoACoAFpiKXJ2Ry+ubFTtL-PuKtez0uycbZ830i1GrNx3JdFdPB73982-HH2djb6Td6nGaIOaTe7ZRTQdCwbavGFww6I2Gwc5pOhI9F3LIdOEvejYlEQolojGkrHkryU+jQAAeAAdiJApIJAkA) for reference. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This rule additionally supports TypeScript type syntax. There are multiple ways to declare global variables in TypeScript. Only using `var` works for all cases. See this [TypeScript playground](https://www.typescriptlang.org/play/?#code/PQgEB4CcFMDNpgOwMbVAGwJYCMC8AiAEwHsBbfUYAPgFgAoew6ZdAQxlAHN1jtX1QAb3qhRGaABdQAGUkAuUAGcJkTIk4ixAN3agAauwXLV6+ptFqJCWK1SgA6mpIB3IebGjHiIyrUa6HgC+9MEMdGDcvPygtqiKivSyEvQGkPReZuHAoM5OxK6ckvS5iC4AdEnFec5lqVWl+WUZYWAlLkpFdG2NSaC4oADkA-XlqX2Dw13VTWrjQ5kRPHzoACoAFpiKXJ2Ry+ubFTtL-PuKtez0uycbZ830i1GrNx3JdFdPB73982-HH2djb6Td6nGaIOaTe7ZRTQdCwbavGFww6I2Gwc5pOhI9F3LIdOEvejYlEQolojGkrHkryU+jQAAeAAdiJApIJAkA) for reference. | |
This rule additionally supports TypeScript type syntax. There are multiple ways to declare global variables in TypeScript. Only using `var` works for all cases. |
I think we can avoid this playground link as we already have examples below and we are working on typescript syntax support in ESLint playground.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we are working on typescript syntax support in ESLint playground.
Neat!
Still, I think the TypeScript playground does add context. The code example alone doesn’t show why it’s ok (better even) to use var
instead of let
inside a declare global
block. I’ll gladly remove it if you’re still not convinced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case, instead of the playground link, how about if we add a code block in the docs page to explain why we need var
in TypeScript with appropriate code comments in block?
you can see this page for reference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine to link to the TypeScript playground for demonstration purposes. Our own playground won't have the type checking so not as solid of an example.
Can you please add mention of this in the docs with an example? This is a subtle point that I don't think is immediately obvious so it would be good to document it. |
I’m having some trouble to properly explain this without going into too much detail and showing code samples. This is why I really feel the need for the TypeScript playground link. Maybe it would be best to
|
Okay! lets do it this way, but can you address the suggestion in #19714 (comment)? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the rule logic - I was thinking this should also apply to top-level definitions in (non-module) declaration files, right? Been a while since I've worked with that particular setup. But something like this?
// foo.d.ts
declare var globalVar: string;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a little minimal repository demonstrating this behavior for reference: https://github.com/kirkwaiblinger/tsglobalvardemo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Come to think of it, it actually doesn't matter whether it's a .d.ts
file or not. The same behavior applies with declare
d variables in ordinary .ts
files.
// foo.ts
// setup
declare var globalVar: string;
declare let globalLet: string;
declare const globalConst: string;
// read unqualified
console.log(globalConst); // works
console.log(globalLet); // works
console.log(globalVar); // works
// write unqualified
globalConst = 'doesnt work';
globalLet = 'works';
globalVar = 'works';
// read on globalThis
console.log(globalThis.globalConst); // doesnt work
console.log(globalThis.globalLet); // doesnt work
console.log(globalThis.globalVar); // works
// write on globalThis
globalThis.globalConst = 'doesnt work';
globalThis.globalLet = 'doesnt work';
globalThis.globalVar = 'works';
// read on self
console.log(self.globalConst); // doesnt work
console.log(self.globalLet); // doesnt work
console.log(self.globalVar); // works
// write on self
self.globalConst = 'doesnt work';
self.globalLet = 'doesnt work';
self.globalVar = 'works';
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What matters is whether TypeScript considers file to be a script or a module. If a module contains imports or exports, it’s a module. Otherwise it’s a script. In a script everything you declare is global. In a module you can declare globals with declare global
.
So this module:
declare global {
var x: number;
}
export {}
is equivalent to this script:
declare var x: number;
However, TypeScript also has the option moduleDetection
. If this is set to force
, everything source file considered a module. This makes it ambiguous whether the latter is a local variable declaration or a global. We could assume default options. The moduleDetection
option doesn’t apply to declaration files.
if (node.kind === "var") { | ||
report(node); | ||
if (node.kind !== "var") { | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In typescript-eslint/typescript-eslint#7941, the proposal is to require var
for global ambient declarations rather than just to permit it. Is the discrepancy in behavior here, where non-var
s are simply ignored, intentional?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I see what you're saying...
However, I think it's not such a good idea to have two separate rules, especially in two separate plugins, handle overlapping and possibly-conflicting logic. It's like if return-await were actually two separate rules for when return await
is required and when it's prohibited, and those two rules were in separate plugins, and one of them didn't even have access to the TS API required for correctness in some cases.
What do you think of the following proposal instead? What if we draw the line such that this rule, no-var
, ignores all TS ambient declarations, even those that shouldn't be var
s, and then the typescript-eslint rule (maybe called something like declare-var
) would have the job of properly enforcing or prohibiting var
s in all cases for declare
d variables. This way detailed TS knowledge stays out of no-var
and the dividing line between what's handled by typescript-eslint and what's handled by no-var
is much more clear.
Concretely, that would mean I propose that all of the following, currently documented as incorrect, would be permitted by no-var
...
declare var x: number
declare namespace ns {
var x: number
}
declare module 'module' {
var x: number
}
and instead would be banned by a new typescript-eslint rule.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be clear, I most prefer having all the logic handled in this no-var
rule, including enforcing declare var
where required, at the possible expense of a niche edge case as mentioned in #19714 (comment).
But if we're not going to enforce declare var
where required in this rule, I think we should draw the line at no-var
ignoring declare var
completely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the most correct implementation is as you suggested in #19714 (comment). no-var
should not be requiring var
anywhere, it should be allowing var
where another option doesn't make sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually this even applies to regular code, not just declarations. For example, the following file is also a global script as far as TypeScript is concerned. It doesn’t even matter if it’s JavaScript or TypeScript syntax.
var a = 1
How far should the no-var
exceptions go? Also how many users will actually run into this? I think most code is written as a module today, or at least that’s often the intention.
There are only 2 cases where the user should definitely use var
:
- Within
declare global
, which is handled by this PR. - Within a TypeScript declaration file that doesn’t contain ESM syntax. This can be detected based on the file name (
*.d.ts
/*.d.cts
/*.d.mts
/*.d.*.ts
). Should I add support for these?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original intent of this rule is to ban the use of var
completely. The goal of this PR is to mark as an exception those places in TypeScript where changing from var
to something else either isn't possible or changes the behavior in an undesirable way.
2. Within a TypeScript declaration file that doesn’t contain ESM syntax. This can be detected based on the file name (
*.d.ts
/*.d.cts
/*.d.mts
/*.d.*.ts
). Should I add support for these?
At this point I'd say no. This feels like a step too far. A better approach for a user is to disable this rule just in those files.
This PR sparked quite some discussion. What still needs to happen for this PR to be ready? The summary as I see it now:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Would like another review before merging.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks!
When declaring a global variable in TypeScript, using
var
vslet
/const
yields different behaviour. A declaration usingvar
yields the expected result.See TypeScript playground
Refs #19173
Refs microsoft/vscode#248075
Refs typescript-eslint/typescript-eslint#2594
Refs typescript-eslint/typescript-eslint#7941
Prerequisites checklist
What is the purpose of this pull request? (put an "X" next to an item)
[ ] Documentation update
[x] Bug fix (template)
[ ] New rule (template)
[ ] Changes an existing rule (template)
[ ] Add autofix to a rule
[ ] Add a CLI option
[ ] Add something to the core
[ ] Other, please explain:
Tell us about your environment (
npx eslint --env-info
):What parser are you using (place an "X" next to just one item)?
[ ]
Default (Espree)
[x]
@typescript-eslint/parser
[ ]
@babel/eslint-parser
[ ]
vue-eslint-parser
[ ]
@angular-eslint/template-parser
[ ]
Other
Please show your full configuration:
Configuration
https://github.com/remcohaszing/eslint
What did you do? Please include the actual source code causing the issue.
What did you expect to happen?
Nothing
What actually happened? Please include the actual, raw output from ESLint.
N/A
What changes did you make? (Give an overview)
The
no-var
rule no longer reports the direct use ofvar
within a TypeScriptdeclare global
block.Is there anything you'd like reviewers to focus on?