[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)