[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  : 116 points
       Date   : 2025-10-01 14:06 UTC (3 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.
        
             | mjmas wrote:
             | GCed languages often also have a mechanism for running code
             | on scope exit. For example Lua's <close> attribute, which
             | runs a metamethod when it goes out of scope rather than
             | having to wait for GC:                 function
             | readfile(name)         local f <close> =
             | assert(io.open(name))         return assert(f:read"a")
             | end
        
           | 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.
        
           | 1718627440 wrote:
           | > An explicit construct for scoping would have been so much
           | clearer to me.
           | 
           | Yes, maybe we could use something similar to parenthesis.
           | Maybe they can look curly. /s
        
         | 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".
        
             | thayne wrote:
             | > the difference between lexical scope and block scope
             | 
             | There isn't a difference between lexical scope and "block"
             | scope. What I think you are referring to as "block" scope,
             | is a subset of lexical scope. The difference between var
             | and let/const is where the boundaries of the lexical scope
             | is.
        
             | sfink wrote:
             | Ah, fair, I didn't actually pay attention to which example
             | you were referring to. That example is specifically about
             | `var` being terrible, not `let/const`.
             | 
             | I was really using your comment as a jumping off point for
             | my rant.
             | 
             | I wouldn't describe `var` declarations as lexical, though.
             | Sure, they have a lexical scope that they get hoisted up to
             | cover, but hoisting is not "just lexical scope". It's
             | unusual.
        
           | 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.
        
             | sfink wrote:
             | Actually, let/const do the opposite of adding flexibility.
             | Simple example: if you have a REPL, it has to cheat (as in,
             | violate the rules of the language) in order to do something
             | sensible for let/const. Once you do `let x;`, you can never
             | declare `x` in the REPL again. In fact, simple typing
             | `console.log(v)` is ambiguous: will you enter `let v` some
             | time in the future or not?
             | 
             | You can't monkey patch lexicals, that is much of their
             | point. Any reference to a lexical variable is a fixed
             | binding.
             | 
             | In practice, this comes up most often for me when I have a
             | script that I want to reload: if there are any toplevel
             | `let/const`, you can't do it. Even worse, `class C { ... }`
             | is _also_ a lexical binding that cannot be replaced or
             | overridden. Personally, I normally use `var` exclusively at
             | the toplevel, and `let /const` exclusively in any other
             | scope. But `class` is painful -- for scripts that I really
             | want to be able to reload, I use `var C = class { ... };`
             | which is fugly but mostly works. And yet, I like lexical
             | scoping anyway, and think it's worth the price. The price
             | didn't have to be quite so high, is all. I would happily
             | take the benefit of avoiding TDZ for the price of disabling
             | hoisting in the specific situations where it no longer
             | makes sense.
             | 
             | I agree that hoisting is a backwards compatibility thing. I
             | just think that the minute optional lexical scoping entered
             | the picture, hoisting no longer made sense. Either one is
             | great, the combination is awful, but it's possible for them
             | to coexist peacefully in a language if you forbid the
             | problematic intersection. TDZ is a hack, a workaround, not
             | peaceful coexistence. (TDZ is basically the same fix as I'm
             | proposing, just done dynamically instead of statically.
             | Which means that JS's static semantics depend on dynamic
             | behavior, when the whole point of lexical scoping is that
             | it's _lexical_.)
        
               | chatmasta wrote:
               | > if there are any toplevel `let/const`, you can't do it
               | [monkeypatch it]
               | 
               | True, but you can at least wrap the entire scope and hack
               | around it. It's not gonna be pretty or maintainable but
               | you can avoid/override the code path that defines the
               | let.
               | 
               | Anecdotally... I've monkeypatched a lot of JavaScript
               | code and I've never been stopped from what I wanted to
               | do, whereas with Python I've hit a dead-end in similar
               | situations. Maybe there's some corner case that's
               | unpatchable but I really think there is always a
               | workaround by the ability to wrap the scope in a closure.
               | Worst case you re-implement the entire logic and change
               | the bit you care about.
        
             | minitech wrote:
             | None of the things you mentioned are clearly related to
             | each other. "It basically means you can always override
             | anything, which allows for monkey patching and proxying and
             | adapter patterns and circular imports" is not true.
             | "They're the reason why JavaScript can have multiple
             | versions of the same package while Python cannot" is
             | _definitely_ not true. (I'm not even sure if you're
             | referring to TDZ or hoisting or lexical scope or whatever
             | other part of the context, but these things are unrelated
             | to every option.)
             | 
             | The premise of "a language like Python that works as you
             | describe" is wrong too, since Python doesn't work like that
             | (it has the same hoisting and TDZ concepts as JavaScript):
             | def g():           def f():               return x
             | x = 4           return f()              print(g())  # 4
        
           | ngruhn wrote:
           | Would mutual recursion still work with this solution? E.g.
           | function f() {          return g()        }        function
           | g() {          return f()        }
        
             | mjmas wrote:
             | It would work just like Lua then and require manually
             | hoisting one of the declarations:                 local b
             | local function a()         return b()       end       b =
             | function()         return a()       end
             | 
             | (Lua's named function statements are just syntatic sugar.
             | For example `local function a()` is equivalent to `local a;
             | a = function()`.)
        
           | prmph wrote:
           | One thing about scope hoisting in JS is that it allows to
           | simulate some aspects of functional programming, where the
           | the order of execution does not necessarily math the order of
           | declaration. I use this all the time to order code elements
           | in a file in order of decreasing importance.
           | 
           | I wish there was explicit support for this though, maybe with
           | a construct like <exression> where <antecedent>, etc, like
           | what Haskell has, instead of having to hack it using
           | functions and var
        
         | 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.
        
         | bigstrat2003 wrote:
         | I mean, it certainly is surprising to me that you can use the
         | variables before they are declared (albeit with undefined
         | values).
        
       | 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
        
               | dwattttt wrote:
               | To be fair, with JS's initialisation rules it does feel
               | like it could have been anywhere.
        
             | 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.
        
             | epolanski wrote:
             | The interesting part here is that javascript interpreters
             | not having to track the TDZ comes with an interesting
             | performance bonus.
        
               | craftkiller wrote:
               | javascript interpreter*
               | 
               | They are only talking about _one_ javascript engine
               | (node). They didn 't test any other engine or go into the
               | implementation in node. For all we know, this might just
               | be a poorly optimized code path in node that needs a
               | little love, but the author didn't bother doing any
               | investigation.
               | 
               | Looking at the linked github issue, jsc doesn't have the
               | performance penalty. It would have been interesting if
               | the author had investigated why and shared that with us.
               | 
               | If this article is about hoisting then this is a well-
               | made high-effort high-value article. If this article is
               | about performance then this is a low-effort low-value
               | summary of a github issue they read.
        
               | thayne wrote:
               | Maybe it could be optimized more but the TDZ
               | fundamentally adds a performance cost, because it
               | requires a runtime check to see if the variable has been
               | initiatalized yet.
               | 
               | Also node used the v8, the same engine as chromium. So
               | this doesn't just affect node, it also affects the
               | majority of the browser share market. oh, and deno uses
               | v8 too.
        
       | 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.
        
         | csto12 wrote:
         | I wonder how many companies are still using Scala.js. Scala was
         | fun to work with, wish it was more popular these days.
        
           | sjrd wrote:
           | Usage of Scala.js is steadily growing. Several indicators
           | suggest that 1 in 5 Scala developers use Scala.js at this
           | point. It's regularly brought up as one of the strongest
           | suits of Scala.
           | 
           | Usage of Scala itself is less shiny if you look at market
           | share. But I believe it's still growing in absolute numbers,
           | only quite slowly.
        
             | coldtea wrote:
             | > _Usage of Scala.js is steadily growing. Several
             | indicators suggest that 1 in 5 Scala developers use
             | Scala.js at this point._
             | 
             | Wow! 4 out of 20 total ain't bad!
             | 
             | Jocking aside the starting set (Scala developers) is
             | already small, so it's not like either it or even less
             | Scala.js is going to be a major player anytime soon.
        
         | onion2k wrote:
         | _`let`s and `const`s incur a significant performance penalty._
         | 
         | Is that still true? Early versions of V8 would do scope checks
         | for things that weren't declared with var but it doesn't do
         | that any more. I _think_ const and let are lowered to var
         | representation at compile time now anyway, so when the code is
         | running they 're the same thing.
        
           | sjrd wrote:
           | I'm sure it can do that in many cases. But if the scopes are
           | a bit complicated, and in particular when variables are
           | captured in lambdas, it's just not possible. The semantics
           | require the TDZ behavior. If you can statically analyze that
           | the TDZ won't be triggered, you can lower to `var`, but
           | otherwise you have to keep the checks.
        
         | lukan wrote:
         | And here I was thinking I should finally embrace using const
         | and let for performance reasons .. shouldn't in theory the
         | compiler has more room for optimization if it knows the
         | variable won't be changed? Or is apperently all the scope
         | checking more expensive?
        
       | 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.
        
           | Goofy_Coyote wrote:
           | Thanks for the tip, and also thanks for the explanation. I
           | understand what's going on here now. Much appreciated
        
       | 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!
        
         | syg wrote:
         | This kind of elision is implemented.
        
       | twistedpair wrote:
       | It feels like the root of the issue is the scoping design of JS
       | itself, which makes tracking TDZ more costly for the interpreter,
       | and the fact that JS is JIT rather than AOT compiled.
       | 
       | I laud the recent efforts to remove the JS from JS tools (Go in
       | TS compiler, esbuild, etc), as you don't need 100% of your lang
       | utils written in the same interpreted lang, especially
       | slow/expensive tasks like compilation.
        
       | conartist6 wrote:
       | Why wouldn't `let` be exactly what you want? It's block scoped
       | but doesn't need fancy TDZ checks because like `var` it just
       | starts out as undefined.
        
         | pverheggen wrote:
         | I think it'll still throw a ReferenceError. Initialization is
         | optional, but you still have to initialize before referencing.
        
           | rictic wrote:
           | Nope. `(() => {let bar; return bar})()` is `undefined`
        
         | strken wrote:
         | The article is slightly wrong. The TDZ is the zone _before_ the
         | variable is declared, not after. Referencing variables before
         | they 're declared isn't valid for `let` and hence it needs a
         | TDZ check.
         | 
         | Consider                   console.log(foo)         let foo
         | 
         | vs                   console.log(foo)         var foo
         | 
         | I think the article confuses "in scope" with "declared", and
         | "declared and initialised" with "initialised".
        
       | pwdisswordfishy wrote:
       | I wish they had made lexical scope work like Lua, where the
       | binding simply does not exist before the declaration - including
       | in the initialization expression:                   local x = 42
       | do           print(x) -- 42           local x = x * 2
       | print(x) -- 84         end         print(x) -- 42
       | 
       | Look, ma! No dead zones!
        
       ___________________________________________________________________
       (page generated 2025-10-04 23:02 UTC)