TypeScript: In "declare global" blocks, allow access of block-scoped variables in globalThis

🔍 Search Terms

globalThis, declare global block, let, const, var, property

✅ Viability Checklist

⭐ Suggestion

This wouldn’t be a breaking change in existing JavaScript code. When it comes to TypeScript code, I’m not as sure


Before my request, here’s an excerpt of #30477 for context:

TypeScript Version: 3.4.0-dev.20190316

Search Terms: 3.4 globalThis let const property

Code

// Compile with `tsc -t es2015`
const foo: number = 42;
const bar: null = globalThis.foo; // Type 'number' is not assignable to type 'null';

For reference, the following demonstrates how these bindings behave in browser and node, respectively:

<script>
const foo = 42;
alert(window.foo); // --> "undefined"
</script>
const foo = 42;
console.log(global.foo); // --> "undefined"

Expected behavior: TypeScript models types consistently with what’s actually happening in VMs. Specifically, variables bound by let or const are not translated to properties on globalThis.

Actual behavior: TypeScript converts let/const bindings onto properties of globalThis, but these properties do not exist in ES2015+ output (when let and const are retained in the output).

The example and fix makes sense to me and I’m not requesting a change for that. But is there any chance #30510 / #30477 could work the way it used to within declare global blocks? In other words, I’m requesting for all of this code to compile:

export {}

declare global {
  const c: string;
  let l: string;
  var v: string;
}

globalThis.c = "s";  // errors: Property 'c' does not exist on type 'typeof globalThis'.
globalThis.l = "s";  // errors: Property 'l' does not exist on type 'typeof globalThis'.
globalThis.v = "s"; // compiles

📃 Motivating Example

The examples in the #30477 excerpt make sense to me because they’re analogous to runtime behavior. e.g., I can intuit const foo: number = 42; shouldn’t exist on globalThis.

But declare global { ... } doesn’t exist at runtime in at all. It’s a part of TypeScript that’s erased at transpile. So if this is TypeScript-only, and it’s named “declare global,” I would assume my consts and lets apply to global scope.

Well it does.

Sometimes.

As a newb, it feels kind of arbitrary. For example, this code compiles like I would expect:

export {}

declare global {
  interface Window {
    foo: number;
  }

  function Y(): void;
}

globalThis.window.foo;
globalThis.Y();

And if I remove globalThis from my first example, it behaves the way I would expect:

export {}

declare global {
  const c: string;
  let l: string;
  var v: string;
}

c = "s";  // Cannot assign to 'c' because it is a constant.
l = "s";  // compiles
v = "s"; // compiles

So, it’s conditionally global?

This stack overflow post shows some conversation around this topic..

đŸ’» Use Cases

  1. What do you want to use this for? I want it to be easier to learn how to declare global types.
  2. What shortcomings exist with current approaches? I think the current approach is nuanced and behaves in unexpected ways.
  3. What workarounds are you using in the meantime? I refer back to these links whenever global variable declarations don’t work the way I expect

About this issue

  • Original URL
  • State: closed
  • Created 7 months ago
  • Reactions: 1
  • Comments: 17 (7 by maintainers)

Most upvoted comments

Cross-script boundaries are totally weird: a declaration will be visible for any subsequent script, but it is only visible to preceding code within its current script.

<script>
console.log(a); // Cannot find variable
</script>
<script>
console.log(a); // undefined
var a = 2;
</script>
<script>
console.log(a); // 2
</script>

I’m not sure what you mean by “native ECMAScript”. ModuleEvaluation and ScriptEvaluation are both top-level operations invoked by the host only, so you need a host to test them out, let it be Node or HTML. You never have access to the script scope in Node, but you can easily do this in HTML:

<script>
const a = 1;
</script>
<script type="module">
console.log(a);
</script>

If it interests you: https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-runtime-semantics-scriptevaluation, https://tc39.es/ecma262/multipage/ecmascript-language-scripts-and-modules.html#sec-source-text-module-record-initialize-environment

BTW, the “nesting” does not happen in a lexical sense. It’s merely that each time a script is evaluated, it directly uses the global scope, while when each module is evaluated, it creates a new scope that inherits from the global scope. There’s no closures.

Maybe someone will discover a reason that I’m wrong, but I think this is just a bug.

This might be the reason why ECMAScript didn’t have a standard global (AKA globalThis) for as long as it did - because it doesn’t behave intuitively. Until globalThis was standardized, we had window (browser) and global (Node), but they were always implementation-specific.

I think “script” is closer-to-the-metal while “global” is more abstract and fits user intuitions better.

I stand corrected. declare script would be worse.

Most people won’t run into the tiny nuances

People tell me this but I wonder
 Could it be that people run into these issues more than it seems, but they lack the knowledge to bring it up? I’m not asking rhetorically, but I’ll offer my own experience:

window, global, and globalThis declarations worked the way I wanted occasionally, but not usually, and I could never get to the bottom of it. For the longest time, I thought it had something to do with the name of the declaration file and/or its path in my project and/or whether it’s in a node_modules subdirectory or not. Until I found this stackoverflow answer, I didn’t know what I didn’t know.

After reading it, I thought I had a solid grasp on what was going on. But it turned out I was mistaken. It wasn’t until this post that I learned it has more to do with JavaScript and “global” than TypeScript.

If I had the pressure of a deadline, I’d probably use (window as any), (global as any), or (globalThis as any) as a solution to get back to the main task at hand. When somebody solves the problem that way, their experience may never be shared with the TypeScript community.

This was kind of a rant, but it’s not directed toward you. I’m trying to learn why my experience feels like the opposite of “Most people won’t run into the tiny nuances.” (It will probably be explained by “Your experience is not the norm,” and I’m fine with that explanation, but I want to confirm with others before going with that assumption.)


@fatcerberus Sorry for reposting this comment. I made such a large edit it felt like a different reply.

in no world would a “global const” become a property on globalThis anyway

I was thinking this could have worked if the property was made non-writable (and non-configurable), but I guess yeah, the weird control-flow sensitive TDZ semantics would make implementing things that way unrealistic.

I think “script” is closer-to-the-metal while “global” is more abstract and fits user intuitions better. Most people won’t run into the tiny nuances—and in no world would a “global const” become a property on globalThis anyway 😉I don’t have strong opinions about the status quo.

TIL, thanks @Josh-Cena

@fatcerberus

Yeah, TS is just JS with compile-time types (plus a few value-add runtime features that got grandfathered in). So const has the same semantics in TS as in JS (or at least is supposed to).

Ah, I meant something slightly different. From that SO post:

On the other side, the following is possible, but the result is different:

export {}

declare global {
  let someLetString: string
  const someConstString: string
}

This only adds types for the following handles:

someLetString
someConstString

[As opposed to window.someLetString and/or globalThis.someLetString]

Although the ts compiler complains about window.someLetString and/or globalThis.someLetString not existing, I mistakenly assumed that during JS runtime, they would evaluate to the same value.