Skip to content

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

Merged
merged 5 commits into from
Jun 6, 2025

Conversation

remcohaszing
Copy link
Contributor

@remcohaszing remcohaszing commented May 8, 2025

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

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):

  • Node version: v22.15.0
  • npm version: v10.9.2
  • Local ESLint version: v9.26.0
  • Global ESLint version: Not found
  • Operating System: linux 6.12.10-76061203-generic

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.

declare global {
  var bar: 'car'
}

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 of var within a TypeScript declare global block.

Is there anything you'd like reviewers to focus on?

@remcohaszing remcohaszing requested a review from a team as a code owner May 8, 2025 18:00
@github-project-automation github-project-automation bot moved this to Needs Triage in Triage May 8, 2025
@eslint-github-bot eslint-github-bot bot added the feature This change adds a new feature to ESLint label May 8, 2025
@github-actions github-actions bot added the rule Relates to ESLint's core rules label May 8, 2025
Copy link

netlify bot commented May 8, 2025

Deploy Preview for docs-eslint ready!

Name Link
🔨 Latest commit b3ab2a9
🔍 Latest deploy log https://app.netlify.com/sites/docs-eslint/deploys/6821db87660d1d0008ea3a32
😎 Deploy Preview https://deploy-preview-19714--docs-eslint.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link
Contributor

@snitin315 snitin315 left a 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.

@snitin315 snitin315 moved this from Needs Triage to Implementing in Triage May 9, 2025
Copy link
Contributor

@Tanujkanti4441 Tanujkanti4441 left a 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

@remcohaszing
Copy link
Contributor Author

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Member

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.

@nzakas
Copy link
Member

nzakas commented May 12, 2025

When declaring a global variable in TypeScript, using var vs let/const yields different behaviour. A declaration using var yields the expected result.

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.

@remcohaszing
Copy link
Contributor Author

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

  1. First remove the playground link as @Tanujkanti4441 suggested;
  2. Implement Rule proposal: prefer declare var for global declarations typescript-eslint/typescript-eslint#2594;
  3. In a follow-up, link to that rule for a deeper explanation.

@Tanujkanti4441
Copy link
Contributor

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

  1. First remove the playground link as @Tanujkanti4441 suggested;

  2. Implement Rule proposal: prefer declare var for global declarations typescript-eslint/typescript-eslint#2594;

  3. In a follow-up, link to that rule for a deeper explanation.

Okay! lets do it this way, but can you address the suggestion in #19714 (comment)?

Copy link
Contributor

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;

Copy link
Contributor

@kirkwaiblinger kirkwaiblinger May 14, 2025

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

Copy link
Contributor

@kirkwaiblinger kirkwaiblinger May 14, 2025

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';

Copy link
Contributor Author

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;
Copy link
Contributor

@kirkwaiblinger kirkwaiblinger May 14, 2025

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s #2594, though there is discussion about it in #7941. I have been fiddling a bit to implement #2594. I don’t think it should have been closed as a duplicate. They’re two different things.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Contributor Author

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:

  1. Within declare global, which is handled by this PR.
  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?

Copy link
Member

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.

@lumirlumir lumirlumir added the accepted There is consensus among the team that this change meets the criteria for inclusion label May 21, 2025
@remcohaszing
Copy link
Contributor Author

This PR sparked quite some discussion. What still needs to happen for this PR to be ready?

The summary as I see it now:

  • People should prefer var over let or const when declaring globals — Out of scope for this rule. Makes sense elsewhere. No action needed (in this PR).
  • There are more false positives that can be detected without a TypeScript context in declaration files — Edge case. People can disable no-var if they have to. No action needed.
  • The link to the TypeScript playground should be removed — Keep it. It’s useful context for demonstration purposes. No action needed.
  • Explanation with an example — This is already explained. The example code requires type-checking to make sense and is on the TypeScript playground. No action needed.

Copy link
Member

@nzakas nzakas left a 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.

@nzakas nzakas moved this from Implementing to Second Review Needed in Triage Jun 4, 2025
Copy link
Contributor

@snitin315 snitin315 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@snitin315 snitin315 merged commit dbba058 into eslint:main Jun 6, 2025
31 checks passed
@github-project-automation github-project-automation bot moved this from Second Review Needed to Complete in Triage Jun 6, 2025
peterhirn added a commit to phi-ag/solid-pages that referenced this pull request Jul 9, 2025
peterhirn added a commit to phi-ag/rvt-app that referenced this pull request Aug 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted There is consensus among the team that this change meets the criteria for inclusion contributor pool feature This change adds a new feature to ESLint rule Relates to ESLint's core rules
Projects
Status: Complete
Development

Successfully merging this pull request may close these issues.

6 participants