The Temporal Dead Zone, or why the TypeScript codebase is littered with var statements
If you have been working with JavaScript for a while, you probably know there are a couple of different ways to initialize a variable. Nowadays we usually use
const password = "hunter2"
and only occasionally, if you need some mutable state:
let password = "hunter2"
These declarations have been around for a while and they have reasonable block scoping rules:
function example(measurement) {
console.log(calculation); // ReferenceError
console.log(anotherCalc); // ReferenceError
if (measurement > 1) {
const calculation = measurement + 1;
let anotherCalc = measurement * 2;
// ...
} else {
// ...
}
console.log(calculation); // ReferenceError
console.log(anotherCalc); // ReferenceError
}
But if you have been working with JS for a really long while,
you might remember the time when neither of these declarations were possible.
All we had was var
. And var
stinks. Not only is
every variable mutable, with no way to enforce immutability, but to make
matters worse, var
leaks beyond block scope:
function example(measurement) {
console.log(calculation); // undefined - accessible! calculation leaked out
console.log(i); // undefined - accessible! i leaked out
if (measurement > 1) {
var calculation = measurement + 1;
// ...
} else {
// ...
}
console.log(calculation); // 1 - accessible! calculation leaked out
for (var i = 0; i < 3; i++) {
// ...
}
console.log(i); // 3 - accessible! i leaked out
}
Terrible!
So it was a big surprise to me to find out that the TypeScript codebase (which is itself written in TypeScript — for now ) is littered with var statements like it's 2003 :
/** @internal */
export function createSourceMapGenerator(
host: EmitHost,
file: string,
sourceRoot: string,
sourcesDirectoryPath: string,
generatorOptions: SourceMapGeneratorOptions
): SourceMapGenerator {
/* eslint-disable no-var */
var { enter, exit } = generatorOptions.extendedDiagnostics
? performance.createTimer("Source Map", "beforeSourcemap", "afterSourcemap")
: performance.nullTimer;
// Current source map file and its index in the sources list
var rawSources: string[] = [];
var sources: string[] = [];
var sourceToSourceIndexMap = new Map<string, number>();
var sourcesContent: (string | null)[] | undefined; // eslint-disable-line no-restricted-syntax
var names: string[] = [];
var nameToNameIndexMap: Map<string, number> | undefined;
var mappingCharCodes: number[] = [];
var mappings = "";
// Last recorded and encoded mappings
var lastGeneratedLine = 0;
var lastGeneratedCharacter = 0;
var lastSourceIndex = 0;
var lastSourceLine = 0;
var lastSourceCharacter = 0;
var lastNameIndex = 0;
var hasLast = false;
// ... etc
}
The reason has to do with what is called the Temporal Dead Zone (TDZ) . For each variable in your code, there is a zone where the variable is declared but not initialized. The following example makes this clear:
function example() {
const result = Math.random() < 0.5 ? useX() : 1; // 50% probability of a ReferenceError
const x = 10;
return result;
function useX() {
return x;
}
}
In the above example, it is totally valid to declare g
at the
bottom of the function. The trouble comes when you call it before
x
has been initialized - in other words, when the interpreter is
still in the TDZ of x
. The interpreter refuses to let you access
x
and throws an error.
And that is great! Because if you would use var
instead of
const
in this example, there would be no error and the function
would simply return undefined!
function example() {
var result = useX(); // undefined
var x = 10;
return result; // undefined
function useX() {
return x;
}
}
console.log(example()); // undefined
Terrible!
So the TDZ is actually a super useful feature brought on by
const
and let
. So why doesn't TypeScript want to use
it? It's because Performance.
Figuring out whether it is in a variable's TDZ is a lot of work for the
interpreter. As you can see above, it cannot be done statically, but depends
on nondeterministic runtime behavior. That brings a penalty which was
significant for the TypeScript codebase. After migrating a decent amount of
their statements to var
,
they saw an 8%
performance improvement across some benchmarks
.
I for one am very happy that I don't have to use var
anymore. And
for TypeScript, I'm sure it's just another reason to
migrate their codebase to Go
.

An abstract painting interpreting the use of the var keyword in JavaScript codebases
Discuss this article on Reddit or Hacker News.
Feel free to email me at mail@vincentrolfs.dev. Or you can follow me on Mastodon at @vincentrolfs@hachyderm.io.