[HN Gopher] The Temporal Dead Zone, or why the TypeScript codeba...
       ___________________________________________________________________
        
       The Temporal Dead Zone, or why the TypeScript codebase is full of
       var statements
        
       Author : vincentrolfs
       Score  : 37 points
       Date   : 2025-10-01 14:06 UTC (2 days ago)
        
 (HTM) web link (vincentrolfs.dev)
 (TXT) w3m dump (vincentrolfs.dev)
        
       | taejavu wrote:
       | The first example is not "terrible", that's just how lexical
       | scope works. I don't really see the point of complaining about
       | language features like this - either learn how it works or ignore
       | at your peril.
        
         | throw-the-towel wrote:
         | I second that, I actually don't understand why do people
         | believe every pair of curly braces has to be its own separate
         | scope. An explicit construct for scoping would have been so
         | much clearer to me.
        
           | samus wrote:
           | In most languages, each block indeed _is_ a separate scope.
           | And it avoids foot guns about accidentally using variables
           | that already serve another purpose. I guess it 's one of the
           | things that are typical for dynamic languages.
        
           | Aurornis wrote:
           | > I actually don't understand why do people believe every
           | pair of curly braces has to be its own separate scope.
           | 
           | It's much easier to reason about when your variables aren't
           | going to escape past the end of the block.
           | 
           | In non-GC languages going out of scope can also be a trigger
           | to free the contents of the variable. This is useful for
           | situations like locking where you can put the minimal span of
           | code that requires the lock into a scope and take a lock
           | which automatically unlocks at the end of the scope, for
           | example.
           | 
           | JavaScript's hoisting and scoping feel natural to people who
           | started in JS, but most people who came from other languages
           | find it surprising.
        
           | thaumasiotes wrote:
           | > An explicit construct for scoping would have been so much
           | clearer to me.
           | 
           | What would be the advantage over the system used everywhere
           | else?
        
           | happytoexplain wrote:
           | >I actually don't understand why do people believe every pair
           | of curly braces has to be its own separate scope
           | 
           | To avoid having to memorize _yet one more thing_ that doesn
           | 't have an obvious benefit.
           | 
           | >An explicit construct for scoping would have been so much
           | clearer to me
           | 
           | Having an additional construct for scoping is clearer than
           | having every set of already-existing curly braces be a new
           | scope? That seems backwards.
        
         | sfink wrote:
         | That's not how lexical scope works anywhere but in JavaScript.
         | Or rather, it's the interaction between "normal" lexical scope
         | and hoisting. In a "normal" lexically scoped language, if you
         | tried:                   function f() {           return x; //
         | Syntax parsing fails here.         }         let x = 4;
         | return f();
         | 
         | you would get the equivalent of a ReferenceError for x when f()
         | tried to use it (well, refer to it) at the commented line. But
         | in JavaScript, this successfully returns 4, because `let`
         | inherits the weird hoisting behavior of `var` and `function`.
         | And it has to, because otherwise this would be really weird:
         | function f1() { return x; }         let x = 4;         function
         | f2() { return x; }         return Math.random() < 0.5 ? f1() :
         | f2();
         | 
         | Would that have a 50/50 chance of returning the outer x? Would
         | the engine have to swap which x is referred to in f1 when x
         | gets initialized?
         | 
         | TDZ is also terrible because the engines have to look up _at
         | runtime_ whether a lexical variable 's binding has been
         | initialized yet. This is one reason (perhaps the main reason?)
         | why they're slower. You can't constant fold even a `const`,
         | because `const v = 7` means at runtime "either 7 or nothing at
         | all, not even null or undefined".
         | 
         | In my opinion, TDZ was a mistake. (Not one I could have
         | predicted at the time, so no shade to the designers.) The right
         | thing to do when introducing let/const would have been to make
         | any capture of a lexical variable disable hoisting of the
         | containing function. So the example from the article (trimmed
         | down a little)                   return Math.random() < 0.5 ?
         | useX() : 1;         let x = 4;         function useX() { return
         | x; }
         | 
         | would raise a ReferenceError for `useX`, because it has not yet
         | been declared at that point in the syntactic scope. Same with
         | the similar                   return Math.random() < 0.5 ? x :
         | 1;         let x = 4;
         | 
         | which in current JavaScript also either returns 1 or throws a
         | ReferenceError. I'm not against hoisting functions, and
         | removing function hoisting would have not been possible anyway.
         | The thing is, that's not "just a function", that's a closure
         | that is capturing something that doesn't exist yet. It's
         | binding to something not in its lexical scope, an uninitialized
         | slot in its static environment. That's a weird special case
         | that has to be handled in the engine and considered in user
         | code. It would be better to just disallow it. (And no, I don't
         | think it would be a big deal for engines to detect that case.
         | They already have to compute captures and bindings.)
         | 
         | Sadly, it's too late now.
        
           | taejavu wrote:
           | That's not the example I'm talking about. I mean where he
           | defines `calculation` within the curly braces of the if
           | statement, then says it "leaked out" because he can log it
           | below the closing brace of the if statement. That's a perfect
           | example of the difference between lexical scope and block
           | scope.
        
             | Jtsummers wrote:
             | >>> The first example is not "terrible"
             | 
             | There are several examples in the blog, and only one is the
             | first. It does not include the "terrible" descriptor after
             | it. So your comment is kind of odd because it doesn't
             | connect to the article at all.
             | 
             | If you mean the first example that's described as
             | "terrible", that's the _second_ example and it 's the one
             | with the leaking loop variable. It kind of is terrible,
             | Python has the same problem (and many others, Python
             | scoping rules are not good). C used to have that problem
             | but they at least had the good sense to fix it.
        
               | taejavu wrote:
               | You're right about my mistake, I should have said "the
               | second code snippet".
        
           | chatmasta wrote:
           | This is not some terrible decision that comes with only
           | downsides. In fact there are quite a few upsides to the
           | flexibility it brings compared to a language like Python that
           | works as you describe.
           | 
           | It basically means you can always override anything, which
           | allows for monkey patching and proxying and adapter patterns
           | and circular imports... These are all nasty things to
           | accidentally encounter, but they can also be powerful tools
           | when used deliberately.
           | 
           | These hoisting tricks all play an important role in ensuring
           | backwards compatibility. And they're the reason why
           | JavaScript can have multiple versions of the same package
           | while Python cannot.
        
         | happytoexplain wrote:
         | I think it's reasonable to have the opinion that the way
         | lexical scoping works in JS is "terrible". You may disagree,
         | but "that's just how it works" isn't a good argument. That line
         | of reasoning is often a rationalization that we make when we
         | are very used to a technology - a sort of hostage situation.
        
           | jvanderbot wrote:
           | In particular if it violates the assumptions of any non
           | native programmer, then it's fair game for gripes.
        
       | craftkiller wrote:
       | This is just javascript variable hoisting:
       | https://developer.mozilla.org/en-US/docs/Glossary/Hoisting
        
         | Aurornis wrote:
         | No, the crux of the article is that using var instead of let or
         | const can produce a performance improvement by reducing the
         | complexity of what the interpreter must track.
         | 
         | They cite a surprising 8% performance boost in some cases by
         | using var.
        
           | craftkiller wrote:
           | By crux you mean the 1 paragraph at the end where it mentions
           | performance? That's basically a footnote to an article that
           | spends the other 99% describing javascript variable hoisting.
           | They cite an 8% performance boost but they don't analyze it,
           | instead just claiming it is a lot of work for the interpreter
           | and linking to a github issue. They've run no benchmarks.
           | They have shown no interpreter internals. They just report
           | that one project saw an 8% performance improvement.
           | 
           | They did a great job of explaining javascript variable
           | hoisting, but that's all that they have explained.
        
             | wonnage wrote:
             | Yes it turns out the article's conclusion is in fact
             | contained in the conclusion paragraph
        
             | pynappo wrote:
             | > By crux you mean the 1 paragraph at the end where it
             | mentions performance? That's basically a footnote to an
             | article that spends the other 99% describing javascript
             | variable hoisting.
             | 
             | Isn't that part still the crux of the article as it
             | contains the answer to the title?
        
         | happytoexplain wrote:
         | Yes. But what are you implying by the word "just"? It sounds
         | like you're saying we should be taking something different away
         | from the article's description of this behavior simply because
         | you have put a name to it.
        
           | craftkiller wrote:
           | Think of it like a tl;dr. Hoisting is common knowledge to
           | javascript programmers, so I've managed to compress the
           | information of this article into 6 words for them.
        
       | adzm wrote:
       | Considering anything that transpiled to ES5 would have to use var
       | anyway, I'm curious why this was done in the source itself and
       | not as a plugin/build step.
        
         | inbx0 wrote:
         | The post links to a TS issue [1] that explains
         | 
         | > As of TypeScript 5.0, the project's output target was
         | switched from es5 to es2018 as part of a transition to
         | ECMAScript modules. This meant that TypeScript could rely on
         | the emit for native (and often more-succinct) syntax supported
         | between ES2015 and ES2018. One might expect that this would
         | unconditionally make things faster, but surprise we encountered
         | was a slowdown from using let and const natively!
         | 
         | So they _don 't_ transpile to ES5, and that is the issue.
         | 
         | 1: https://github.com/microsoft/TypeScript/issues/52924
        
       | benatkin wrote:
       | To me it isn't unlike react having onChange={<function to be
       | called when the input event fires>}
       | 
       | I can always rely on FAANGs to make things unnecessarily
       | confusing and ugly.
        
         | sorrythanks wrote:
         | i don't understand the connection
        
       | sjrd wrote:
       | Indeed, `let`s and `const`s incur a significant performance
       | penalty. This is also why the Scala.js compiler emits `var`s by
       | default, even when targeting very recent versions of ECMAScript.
       | 
       | The good news is that we can still write our Scala `val`s and
       | `var`s (`const` and `let`) in the source code, enjoying good
       | scoping _and_ good performance.
        
       | Goofy_Coyote wrote:
       | Need some help understanding what's going on here.
       | 
       | In
       | 
       | ```
       | 
       | function example(measurement) {
       | console.log(calculation); // undefined - accessible! calculation
       | leaked out            console.log(i); // undefined - accessible!
       | i leaked out
       | 
       | <snip>
       | 
       | ```
       | 
       | Why does the author say `calculation` and `i` are leaking?
       | They're not even defined at that point (they come later in the
       | code), and we're seeing "undefined" which, correct me if I'm
       | wrong, is the JS way of saying "I have no idea what this thing
       | is". So where's the leakage?
        
         | Jtsummers wrote:
         | Two spaces before each line in the code block. HN doesn't use
         | markdown, it's easy to do even on mobile, a demonstration:
         | function example(measurement) {
         | console.log(calculation); // undefined - accessible!
         | calculation leaked out         console.log(i); // undefined -
         | accessible! i leaked out <snip>
         | 
         | It's "leaking" because the variable is in scope, it's
         | associated value is "undefined". This is different than with
         | let/const where the variable would _not_ be in scope at that
         | point in the function. An undefined value bound to a variable
         | is _not_ the same as  "I have no idea what this thing is". That
         | would be the reference errors seen with let/const.
        
       | spankalee wrote:
       | I've tried to get V8 at least to implement smarter TDZ elision
       | for a long time, which would eliminate the need for these
       | shenanigans.
       | 
       | There are some relatively simple heuristics where you can tell
       | without escape analysis that a variable will not be referenced
       | before initialization.
       | 
       | The obviously bad constructions are references in the same scope
       | that happen before the declaration. It'd be nice if these were an
       | early errors, but alas, so keep the TDZ check. The next is any
       | closed over reference that happens before the initializer. These
       | may run before the initializer, so keep the TDZ check. Then you
       | have hoisted closures even if they're after the initializer (eg,
       | var and function keyword declarations). These might run before
       | the initializer too.
       | 
       | But everything else that comes after the initializer: access in
       | the same or nested scope and access in closures in non-hoisted
       | declarations, can't possibly run before the initializer and
       | doesn't need the TDZ check.
       | 
       | I believe this check is cheap enough to run during parsing. The
       | reason for not pursuing it was that there wasn't a benchmark that
       | showed TDZ checks were a problem. But TypeScript showed they
       | were!
        
       ___________________________________________________________________
       (page generated 2025-10-03 23:00 UTC)