Skip to content
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

[Discussion] Better Way to Declare Globals in JavaScript files #15626

Open
mjbvz opened this issue May 6, 2017 · 43 comments
Open

[Discussion] Better Way to Declare Globals in JavaScript files #15626

mjbvz opened this issue May 6, 2017 · 43 comments
Assignees
Labels
Domain: JavaScript The issue relates to JavaScript specifically Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript VS Code Tracked There is a VS Code equivalent to this issue

Comments

@mjbvz
Copy link
Contributor

mjbvz commented May 6, 2017

Problem
Using the new checkJs feature, it is currently difficult to tell TypeScript about a global variable:

// @ts-check

myGlobal

window.myVar = 'foo';

screen shot 2017-05-05 at 4 44 20 pm

The only way around this that I know is to create a .d.ts file that defines these:

interface Window {
    myVar: string;
}

declare var myGlobal: string;

JavaScript users should not have to write *.d.ts files.

Possible Approaches
@egamma proposes that may be able to use the global comment from ESlint http://eslint.org/docs/user-guide/configuring : /* global var1, var2 */

I'd also be interested to see if we could put type information in a jsdoc somehow, something like:

/**
 * @global {string} myGlobal 
 */
@mhegazy
Copy link
Contributor

mhegazy commented May 6, 2017

JavaScript users should not have to write *.d.ts files.

Why is that? Js users are already using dts files from @types; so does not seem totally foreign concept.

@Zarel
Copy link

Zarel commented May 6, 2017

Why is that?

I mean, it's a long time since the C/C++ days when we had to manually write header files. There's no reason to force a programmer to write a file describing a variable that TypeScript's already aware of and understands the typing of.

The job of a computer is to do easily-automated tasks for humans. If you are telling a human to do an easily-automated task for a computer, something is seriously wrong.

@mhegazy
Copy link
Contributor

mhegazy commented May 7, 2017

The OP suggests using jsdoc. So some one has to write something. Why not in a .d.ts file

@DanielRosenwasser
Copy link
Member

Yes, you can use .d.ts files, but the point of this feature is lower barrier to entry. Plus, if I'm writing a quick one-off script, it can be nice to not worry about such details.

@weswigham
Copy link
Member

If you don't want to write a .d.ts, there's no need to overthink it, you can just annotate a var declaration in the global scope, like so:

/**
*  @type {string}
*/
var myString;

then later statements like:

console.log(myString.toUpperCase());

will be typechecked.

Even if you're writing modules, you can add a file containing globals like this to your compilation - just don't write any imports or exports and it should be global scoped as expected.

Unfortunately, as far as I'm aware there's no way to patch definitions onto existing types at present using only jsdoc syntax, so for that you'd have to use a .d.ts. - the correct way, I believe, would be to interpret @memberof jsdoc keywords as indicating a type patching operation, and then using those, but AFAIK TS doesn't treat @memberof in any special way right now.

@egamma
Copy link
Member

egamma commented May 8, 2017

When discussing the options it would also be good to provide a quick fix for such an error.

(we did that in VS Code before we switched to Salsa by providing a quick fix that added eslint global comment /* global var */ to the file)

@mhegazy mhegazy added Salsa Suggestion An idea for TypeScript labels May 8, 2017
@mjbvz
Copy link
Contributor Author

mjbvz commented May 9, 2017

I've added some documentation to VSCode on the d.ts workaround for global variables: microsoft/vscode-docs#991

My main concern with d.ts files is that regular JavaScript programmers may not have any experience with them. JSDocs or eslint style directives may be less intimidating and they also wouldn't require creating a jsconfig.json like the d.ts based approach does

@Zarel
Copy link

Zarel commented May 9, 2017

@weswigham

Even if you're writing modules, you can add a file containing globals like this to your compilation - just don't write any imports or exports and it should be global scoped as expected.

Unfortunately, this doesn't work for @typedef types/interfaces.

I'm still not sure how I use types defined in other files at all...

@rbuckton
Copy link
Member

usejsdoc.org specifies two existing JSDoc tags that could be useful here:

  • @global - does not support name/type itself
  • @var - example: /** @var {*} myGlobal */

@mhegazy
Copy link
Contributor

mhegazy commented May 10, 2017

based on the discussion in #15747, we need to support @var as the JSDoc representation of declare var .. in the global scope.

@mhegazy
Copy link
Contributor

mhegazy commented May 10, 2017

adding globals to window is tracked under #12902

@mhegazy mhegazy added this to the TypeScript 2.4 milestone May 10, 2017
@mhegazy mhegazy modified the milestones: TypeScript 2.4, TypeScript 2.5 Jun 5, 2017
@thw0rted
Copy link

As a vanilla-JS user who just started using checkJs, it would be really nice if my eslint comment (/* global myVar */) worked with TS so I didn't have to document my global in two different ways for two different checkers. I hope that the TypeScript team supports the goal of checkJs working on any valid JS without having to add a .d.ts file to your build just to make the checker happy.

@mhegazy mhegazy modified the milestone: TypeScript 2.6 Sep 1, 2017
@RyanCavanaugh RyanCavanaugh added the Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. label Aug 16, 2018
@weswigham weswigham added Domain: JavaScript The issue relates to JavaScript specifically and removed Salsa labels Nov 29, 2018
@TotallyInformation
Copy link

Any movement on this proposal?

@xileftenurb
Copy link

About this, I did not find any ways to add a property to the node global object without modifing the NodeJS.global definition who is common for every project on a computer (and not in the project.)

the node global var is already declared, so I cannot use "@type" anotation as declaration
I cannot use a d.ts since it already existe, outside the scope of the project.

anyones as an idea on how to fix that?

I would like to be able to document code as simple as
global.__rootdir = __dirname
in node so i can use global.__rootdir in any file of my project.

@dcsan
Copy link

dcsan commented May 30, 2019

@xileftenurb I have exact same problem with some code I inherited. Did you find a workaround?

@xileftenurb
Copy link

the only workaround I found is to do at the start of the file who use the global :

// @ts-ignore
const rootdir = global.__rootdir

and to use the newly created variable in the file

@thw0rted
Copy link

thw0rted commented May 30, 2019

If you look upthread, they discuss the correct syntax for declaring globals in a .d.ts (ambient type definition) file. You'd then configure your tsconfig.json to point to that file, and subsequent runs of tsc will have type information for the globals you need. You say you can't use a .d.ts file but I don't understand why not?

ETA: to be clear, I agree with the OP that one shouldn't need to write a .d.ts file to make your JS pass the type-checker, but the issue is still open because that capability hasn't been implemented yet.

@dcsan
Copy link

dcsan commented May 30, 2019

You'd then configure your tsconfig.json to point to that file

can you explain how to do this?
I did try editing a .d.ts but it didn't seem to get picked up.
reference

also partially the problem for me is that someone is using global.X = {}; in a project I'm working on, and so i need to declare that global to have an X which is a node thing usually only declared in one place on your system.

@thw0rted
Copy link

This seems to work for me:

/* tsconfig.json */
{
  "compilerOptions": {
    ....
    "allowJs": true,                       /* Allow javascript files to be compiled. */
    "checkJs": true,                       /* Report errors in .js files. */
    ....
    "typeRoots": ["./typings", "./node_modules/@types"],                       /* List of folders to include type definitions from. */
    ....
  }
}

/* typings/globals.d.ts */
declare namespace NodeJS {
    interface Global {
        x: number;
    }
}

/* src/main.js */
console.log(global.x); // no error
console.log(global.y); // Property 'y' does not exist on type 'Global'

The thing to be aware of is that the typings for NodeJS (@types/node) that get included by default have the following:

declare var global: NodeJS.Global;
....
declare namespace NodeJS {
  interface Global {
    ....
    process: Process;
    root: Root;
    // etc etc
  }
}

This means it's declaring that global is a global variable, of type NodeJS.Global, and it defines the shape of that interface. When you make another .d.ts file with its own declare namespace NodeJS { interface Global { .... } }, you're augmenting that interface with additional properties. Both of these declarations apply to every file the compiler touches -- the types are merged. This means that whoever provides global.X = {} can also include a .d.ts file that adds X to the NodeJS.Global interface. The mechanism for specifying where that .d.ts file goes varies depending on how you're importing the library.

@dcsan
Copy link

dcsan commented May 30, 2019

thanks! that seems to be working!

@TotallyInformation
Copy link

Apologies that I'm not familiar enough as yet with Typescript but I have a couple of folders in my Node.js app that are actually front-end code and so have radically different globals to the rest of the code.

What would be the correct way to make sure I can check them properly? I am reluctant to put a tsconfig.json file in their root folder as those folders are actually made outwardly available via ExpressJS - though I can do so if needed.

@thw0rted
Copy link

@TotallyInformation you need to make separate projects for frontend vs backend, with their own tsconfig.json. You can use the typeRoots option (as seen in my previous comment here) to keep the .d.ts files in a directory that's different from your JS sources.

Further "how do I" questions would probably be better for StackOverflow -- nothing personal, but we're drifting from the original intent of the issue, which is that (IMHO) TypeScript should provide an inline syntax for declaring globals that doesn't require introduction of a separate .d.ts file. I'd like to see TS support the comment syntax used by eslint.

@TotallyInformation
Copy link

Thanks for the steer @thw0rted. No intent to turn this into a QA session :)

@dcsan
Copy link

dcsan commented Jun 1, 2019

@TotallyInformation why not? this is very helpful and this issue ranks high in google for the question. mute the thread if you prefer, it is a [Discussion]

@thw0rted
Copy link

thw0rted commented Jun 3, 2019

Discussion means that this is the place to debate how best to implement a new or improved feature in Typescript. It does not mean that the thread is open for questions about how to use existing capabilities. If you click the "New Issue" button, you'll see a prompt that includes the text

The issue tracker is not for questions. Please use Stack Overflow or other resources for help writing TypeScript code.

The presence of a "discussion" tag here does not change that guidance.

@heroboy
Copy link

heroboy commented Aug 21, 2019

How I can export a class in window?
For example:

(function(){
    class Foo()
    {
    }
   window.Foo = Foo;
})();

If I use .d.ts, I must declare the class Foo again in .d.ts.

@Paril
Copy link

Paril commented Oct 7, 2019

I'm hoping this still gets some traction. It'd seem like a fairly easy thing to have JS files in your workspace with @global's apply globally like they should; maybe I'm missing some detail here, but VSCode already picks up global functions and classes from various parts of my workspace. Being able to just plop in a @global on a definition and expect it to work as if it's part of globalThis would be amazing/preferred.

@bassem-mf
Copy link

bassem-mf commented Feb 18, 2020

The code that @heroboy posted above is very common in JavaScript programs. A lot of people wrap all the code in the JS file in a self executing function to avoid polluting the global namespace.
Here is what I do currently to document my global functions defined inside other functions.

/**
* @callback MyNamespace1.MyNamespace2.MyNamespace3.myFunction
* @param {string[]} arg1
* @param {any} arg2
* @param {number} arg3
*/

(function () {
    'use strict';

    /** @type MyNamespace1.MyNamespace2.MyNamespace3.myFunction */
    window.MyNamespace1.MyNamespace2.MyNamespace3.myFunction =
        function (arg1, arg2, arg3) {
            
        };

    // Other code that should not pollute the global namespace.
})();

/** @type MyNamespace1.MyNamespace2.MyNamespace3.myFunction */
window.MyNamespace1.MyNamespace2.MyNamespace3.myFunction;

You see there is a lot of repetition and the code is not very elegant. But this is the best way I could find to get intellisense in Visual Studio. I hope we will have a better solution in the future based on @global or @var.

@olleharstedt
Copy link

NB: The Flow syntax to do this is (using jQuery as an example):

/*:: declare var $: Object */

This can be done inline in any .js file using // @flow.

@tnguyen14
Copy link

The only way around this that I know is to create a .d.ts file that defines these

I tried this but it's still not working. Does anyone know how I can get .d.ts file to work?

I have tried both of these in a global.d.ts file

declare var myGlobal: string;
interface Window {
  myGlobal: string;
}

@bfulop
Copy link

bfulop commented Oct 4, 2021

Hi @tnguyen14 ,

I had a similar issue as you. In my experience if your ambient type declaration file contains an import or export statement, then it's no longer considered to be a global declaration file.

Edit: you need to move the import/export declaration to a separate file and keep one .d.ts file just to declare globally available types/variables.

@TotallyInformation
Copy link

Hi @tnguyen14 ,

I had a similar issue as you. In my experience if your ambient type declaration file contains an import or export statement, then it's no longer considered to be a global declaration file.

Edit: you need to move the import/export declaration to a separate file and keep one .d.ts file just to declare globally available types/variables.

Thank you!!! Fixed a very painful problem for me.

@solarispika
Copy link

I had a similar question on how to declare a global variable whose type is given by some other module.
But I can't import/require any module in my JavaScript code (due to architecture of our development flow).
After struggling for several weeks intermittently, I found how to do it by a simple ambient module!
I do not set any type related configs in tsconfig.json, so I think it is quite simple.

Here is how I do it.
I prepare an ambient module, say global.d.ts, with the following content

import Target from 'path/to/module';
declare global {
  var myObj: typeof Target;
}

And done!
For more detailed explanation, one can refer to this SO post.
Hope this sharing helps someone :-D

@dmchurch
Copy link

dmchurch commented Apr 9, 2024

I sure do hate necroposting an issue like this, but from what I can tell, it still isn't resolved despite the seven-year-old resolution on using the @var syntax. Why is there still no way to declare global variables using JSDoc? We know the JSDoc syntax, we know the equivalent TS syntax. Why have these not been linked together yet?

For context, not that it particularly matters given that both the problem and the solution are fully specified, I'm trying to make a toy Electron project, using their contextBridge functionality to expose new globals to the renderer's JS environment. I do not have, nor do I want, a jsconfig.json file, a .d.ts file, a VSCode extension/configuration, or a package.json setting. I'm very familiar with all those workarounds. I am not looking for a workaround. All that I want is to have some way to tell the editor "in this file, the identifier foo is available in the global namespace, with type bar". This is the same issue I run into when I try to add typings to a one-off file intended to run outside the typical environment, like Worker or ServiceWorker code in a project that mostly runs in a DOM Window environment, and it frustrates me just as much then.

If it's simply a matter of "no one has done the work yet" I'd be delighted to submit a PR for it, but my experience with corporate-owned software projects like this one is that it is 100% futile to submit a PR without someone on the other side ready and waiting to receive it. That plus, the fact that this would be such a simple issue to knock out and yet it's been seven years tells me that the issue isn't getting the work done, the issue is getting it accepted.

So? What's the hold-up? Is there anything that can be done to move this along, or is this just a pocket veto by the Typescript team?

@trusktr
Copy link
Contributor

trusktr commented Jul 16, 2024

This would be so useful.

When writing Custom Elements with JS + JSDoc, I have one file per custom elements, for example for two elements <my-element> and <other-element> I have these two files:

my-element.js
other-element.js

where one has a MyElement class and the other has a OtherElement class, one class per file per element definition.

To define type definitions for these elements so that they are visible in DOM APIs, for example so that we get this:

const el = document.querySelector('my-element') // type of `el` is `MyElement`

/** @type {MyElement} */
const el2 = el // ok, el is assignable

/** @type {OtherElement} */
const el3 = el // type error, el is not OtherElement but MyElement

we would need to write definitions like this if we were using TypeScript files:

// my-element.js
class MyElement extends HTMLElement {...}
customElements.define('my-element', MyElement)

declare global {
  interface HTMLElementTagNameMap {
    'my-element': MyElement
  }
}
// other-element.js
class OtherElement extends HTMLElement {...}
customElements.define('other-element', OtherElement)

declare global {
  interface HTMLElementTagNameMap {
    'other-element': OtherElement
  }
}

But as you know, we can't do this in a JS file using JSDoc. So, what we have to do is double the number of files that we have, so that we can place these definitions in .d.ts files:

my-element.js
my-element-types.d.ts
other-element.js
other-element-types.d.ts

(Note, the names must not be the same, for example my-element.d.ts would cause TS to see the file as a declaration for the sibling file, hence I have added -types to prevent this from happening, and instead the files are seen as files containing global types).

If we have 50 elements, now we have to double our files to 100, cluttering up the workspace. It would be great not to have to double the amount of files.

An alternative is to stick element definition all in a single file, but then that becomes less portable, for example you can't simply copy/paste files from project to project, but you have to specifically extract pieces of the .d.ts file that has the element defs.

Note, in related issues such as

having a more useful comment space for writing TS types could help. For example, picking one of those syntax proposals at random, we could have something like the following in a JS file:

// other-element.js
class OtherElement extends HTMLElement {...}
customElements.define('other-element', OtherElement)

/*::
declare global {
  interface HTMLElementTagNameMap {
    'other-element': OtherElement
  }
}
*/

@LoganTann
Copy link

LoganTann commented Sep 10, 2024

Also, .d.ts files might not be supported by vscode in some edge cases (having files without the js extension) : microsoft/vscode#37772

The only way to fix this would be to have this proposal for jsdoc

EDIT : To load a d.ts file without the need of a jsconfig file, you can use the following line :

/// <reference path="../lib.d.ts"/>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: JavaScript The issue relates to JavaScript specifically Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript VS Code Tracked There is a VS Code equivalent to this issue
Projects
None yet
Development

No branches or pull requests