You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
block scoped variables erroneously error on declaration order #4244
The following example does not compile, claiming the block-scoped variables are used before declaration, which is not true. The equivalent JavaScript version executes correctly.
In ECMAScript 6, let does not hoist the variable to the top of the block. If you reference a variable in a block before the let declaration for that variable is encountered, this results in a ReferenceError, because the variable is in a "temporal dead zone" from the start of the block until the declaration is processed.
Quite obviously this JS never actually executes foo, so any runtime binding errors aren't found by executing it as written (unless, say, by static analysis by the TS compiler). But, say we do run this JS and then call foo(), like so:
functionfoo(){a=13;varc=b;}leta;constb=42;foo();
... it executes without error. Why is that? That is because the let a; declaration has been bound by the time we've called foo(). If you try this:
functionfoo(){a=13;varc=b;}foo();leta;constb=42;
You'll get a ReferenceError because you've attempted to access a before it was declared. Since let declarations are dependent on having been executed before being accessed, you can construct situations such as this where it is possible to initialize the binding after the apparent usage and still have that usage be valid (whoa).
Perhaps more interestingly, this means I can do this:
and the first call to foo results in a ReferenceError because the let a from the inner scope has already "blocked" a from being read until it is executed.
IMO, this is silly (I certainly see let as something that was supposed to make JS scoping more clear), but after reading over the spec one can see why it's the case.
So back to the issue at the top - is it correct for the TS compiler to error on the above? With just that snippet, nothing else involved? We can emit valid JS for that - for sure - but we would emit JS which it is possible to incur the temporal let ReferenceError error - for example, if this chunk is emitted in the middle of a set of files that get concatenated and foo gets called before let a is executed (this is valid, since function declarations are hoisted!). In isolation the TS compiler can't know if foo will be called before or after the let a and only one of those is valid runtime behavior. It's certainly safer to error and let the programmer know that this is unsafe (I know I'd want to know).
Still, in isolation the above contains no syntactical errors and the usage of let and the inner function is technically valid, if potentially unsafe.
My particular use case which demonstrated this problem had to do with namespace-level variables. I prefer to structure my code with the exported "public" symbols at the top of the namespace, and the inaccessible "private" variables at the bottom. This allows a user/reader to immediately see what can be consumed from the public API without having to wade through a bunch of implementation details.
So, it was something like the following:
namespaceproject{exportclassPublicClass{publicMethod(thing: any): void{letdata=extraData.get(thing);// do stuff with data}private_id=globalID++;}letglobalID=0;constextraData=newWeakMap();}
This is only safe because of the implementation details of TS namespaces - in effect, abusing the namespace system to guarantee safety. If your code wasn't automatically wrapped in an IIFE and executed in order by the TS emit it would be unsafe (but valid) JS. The sad truth is that when you abuse let in this manner if you had wanted to instantiate PublicClass and increment globalID in the static part of your namespace, there is the very real possibility of that being an error.
The perfectionist in me says this could go back to being a flaw in the ES6 spec for let and const - that block-level declarations can be captured by inner functions when the declaration has yet to be executed feels wrong - and against the "intuitiveness" (I declare it and it's available in all the code after it and never the code before it) that let and const are supposed to bring. In effect, the binding of a function is exempt from the block declaration "temporal deadzone".
Anyways, TS could probably add static analysis to detect when it's safe to use let like this, since it is valid sometimes? I don't particularly like that it even can be valid, myself, but that's generally what linters are for, I suppose.
As Wesley Wigham (@weswigham) explained earlier, there is not a way to know for sure if this is a runtime error or not. as it depends on execution order. We could do control flow analysis as well as alias analysis to identify cases where it would be safe to use the declaration.
Alternatively we could relax the restriction of being "syntactically declared before used", but that means we are leaving more possible runtime errors unflagged.
I think we should error on the side of allowing anything that might work. It's a temporal dead zone, not a lexical dead zone, after all. The only case we should error on is where the use occurs in the same scope (i.e. not a function body) prior to the declaration.
Aye. I personally don't like that it even can work, but since it can by the JS spec it should probably be supported. But the question then becomes should we relax the requirements on let scope checking, or should we do that but also add the analysis Mohamed Hegazy (@mhegazy) mentioned to detect uses within the temporal deadzone?
As Ryan Cavanaugh (@RyanCavanaugh) mentioned, it is only function bodies, not block scopes. so the two cases you reference are still errors, only something like a below would be allowed.
functionfoo(){a;// No Error}leta;if(false){b;// still an error}letb;
Another argument for loosening the checks (or tightening them, depending on perspective) is that this example compiles fine, when it clearly violates temporal dead zone semantics:
Also, some people like to put all their functions declarations at the top, even if they reference entities declared later in the code. The strict policy makes it impossible to adhere to this style for those who prefer it. And I think it is a common enough preference.
This should be fixed now. errors are only reported within the same function if the use site is after the declaration, as it is guaranteed to fail, otherwise, no error is reported.
Activity
weswigham commentedon Aug 9, 2015
Background from here:
and from the spec.
The equivalent JS in question:
Quite obviously this JS never actually executes
foo, so any runtime binding errors aren't found by executing it as written (unless, say, by static analysis by the TS compiler). But, say we do run this JS and then callfoo(), like so:... it executes without error. Why is that? That is because the
let a;declaration has been bound by the time we've calledfoo(). If you try this:You'll get a
ReferenceErrorbecause you've attempted to accessabefore it was declared. Sinceletdeclarations are dependent on having been executed before being accessed, you can construct situations such as this where it is possible to initialize the binding after the apparent usage and still have that usage be valid (whoa).Perhaps more interestingly, this means I can do this:
and the first call to
fooresults in aReferenceErrorbecause thelet afrom the inner scope has already "blocked"afrom being read until it is executed.IMO, this is silly (I certainly see
letas something that was supposed to make JS scoping more clear), but after reading over the spec one can see why it's the case.So back to the issue at the top - is it correct for the TS compiler to error on the above? With just that snippet, nothing else involved? We can emit valid JS for that - for sure - but we would emit JS which it is possible to incur the temporal let
ReferenceErrorerror - for example, if this chunk is emitted in the middle of a set of files that get concatenated andfoogets called beforelet ais executed (this is valid, since function declarations are hoisted!). In isolation the TS compiler can't know iffoowill be called before or after thelet aand only one of those is valid runtime behavior. It's certainly safer to error and let the programmer know that this is unsafe (I know I'd want to know).Still, in isolation the above contains no syntactical errors and the usage of
letand the inner function is technically valid, if potentially unsafe.sccolbert commentedon Aug 9, 2015
My particular use case which demonstrated this problem had to do with namespace-level variables. I prefer to structure my code with the exported "public" symbols at the top of the namespace, and the inaccessible "private" variables at the bottom. This allows a user/reader to immediately see what can be consumed from the public API without having to wade through a bunch of implementation details.
So, it was something like the following:
weswigham commentedon Aug 9, 2015
This is only safe because of the implementation details of TS
namespaces - in effect, abusing thenamespacesystem to guarantee safety. If your code wasn't automatically wrapped in an IIFE and executed in order by the TS emit it would be unsafe (but valid) JS. The sad truth is that when you abuseletin this manner if you had wanted to instantiatePublicClassand incrementglobalIDin the static part of your namespace, there is the very real possibility of that being an error.The perfectionist in me says this could go back to being a flaw in the ES6 spec for
letandconst- that block-level declarations can be captured by inner functions when the declaration has yet to be executed feels wrong - and against the "intuitiveness" (I declare it and it's available in all the code after it and never the code before it) thatletandconstare supposed to bring. In effect, the binding of a function is exempt from the block declaration "temporal deadzone".Anyways, TS could probably add static analysis to detect when it's safe to use
letlike this, since it is valid sometimes? I don't particularly like that it even can be valid, myself, but that's generally what linters are for, I suppose.mhegazy commentedon Aug 10, 2015
As Wesley Wigham (@weswigham) explained earlier, there is not a way to know for sure if this is a runtime error or not. as it depends on execution order. We could do control flow analysis as well as alias analysis to identify cases where it would be safe to use the declaration.
Alternatively we could relax the restriction of being "syntactically declared before used", but that means we are leaving more possible runtime errors unflagged.
RyanCavanaugh commentedon Aug 10, 2015
I think we should error on the side of allowing anything that might work. It's a temporal dead zone, not a lexical dead zone, after all. The only case we should error on is where the use occurs in the same scope (i.e. not a function body) prior to the declaration.
weswigham commentedon Aug 10, 2015
Aye. I personally don't like that it even can work, but since it can by the JS spec it should probably be supported. But the question then becomes should we relax the requirements on
letscope checking, or should we do that but also add the analysis Mohamed Hegazy (@mhegazy) mentioned to detect uses within the temporal deadzone?Arnavion commentedon Aug 12, 2015
Ryan Cavanaugh (@RyanCavanaugh) By "same scope" do you mean lexical scope? So you won't error in this case:
because they're in different scopes, even though the assignment is obviously executed in the TDZ of
a?If you do decide to complain in that case, then how will you differentiate it from this case:
where the assignment is not executed in the TDZ of
a?I assume you'll not complain in either case.
mhegazy commentedon Aug 12, 2015
As Ryan Cavanaugh (@RyanCavanaugh) mentioned, it is only function bodies, not block scopes. so the two cases you reference are still errors, only something like
abelow would be allowed.Adding tests for #4244
If the use of the block scoped variable occurs inside function do not…
sccolbert commentedon Dec 22, 2015
Another argument for loosening the checks (or tightening them, depending on perspective) is that this example compiles fine, when it clearly violates temporal dead zone semantics:
JsonFreeman commentedon Dec 22, 2015
Also, some people like to put all their functions declarations at the top, even if they reference entities declared later in the code. The strict policy makes it impossible to adhere to this style for those who prefer it. And I think it is a common enough preference.
mhegazy commentedon Jan 5, 2016
This should be fixed now. errors are only reported within the same function if the use site is after the declaration, as it is guaranteed to fail, otherwise, no error is reported.
sccolbert commentedon Jan 5, 2016
👍 thanks!