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
- This wouldnât be a breaking change in existing TypeScript/JavaScript code
- This wouldnât change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isnât a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isnât a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
â 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
letorconstare not translated to properties onglobalThis.Actual behavior: TypeScript converts
let/constbindings onto properties ofglobalThis, but these properties do not exist in ES2015+ output (whenletandconstare 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
- What do you want to use this for? I want it to be easier to learn how to declare global types.
- What shortcomings exist with current approaches? I think the current approach is nuanced and behaves in unexpected ways.
- 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)
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.
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:
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(AKAglobalThis) for as long as it did - because it doesnât behave intuitively. UntilglobalThiswas standardized, we hadwindow(browser) andglobal(Node), but they were always implementation-specific.I stand corrected.
declare scriptwould be worse.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, andglobalThisdeclarations 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 anode_modulessubdirectory 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.
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
globalThisanyway đI donât have strong opinions about the status quo.TIL, thanks @Josh-Cena
@fatcerberus
Ah, I meant something slightly different. From that SO post:
Although the ts compiler complains about
window.someLetStringand/orglobalThis.someLetStringnot existing, I mistakenly assumed that during JS runtime, they would evaluate to the same value.