-
-
Notifications
You must be signed in to change notification settings - Fork 4.9k
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. |
snitin315
left a 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.
Thanks for picking this up, few comments.
Tanujkanti4441
left a 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.
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 |
|
|
||
| ::: | ||
|
|
||
| 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 declared 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-vars 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 vars, and then the typescript-eslint rule (maybe called something like declare-var) would have the job of properly enforcing or prohibiting vars in all cases for declared 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 = 1How 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:
|
nzakas
left a 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.
LGTM. Would like another review before merging.
snitin315
left a 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.
LGTM, thanks!
When declaring a global variable in TypeScript, using
varvslet/constyields different behaviour. A declaration usingvaryields 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[ ]
OtherPlease 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-varrule no longer reports the direct use ofvarwithin a TypeScriptdeclare globalblock.Is there anything you'd like reviewers to focus on?