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