Was this page helpful?

Variable Declaration

let andconst are two relatively new concepts for variable declarations in JavaScript.As we mentioned earlier,let is similar tovar in some respects, but allows users to avoid some of the common “gotchas” that users run into in JavaScript.

const is an augmentation oflet in that it prevents re-assignment to a variable.

With TypeScript being an extension of JavaScript, the language naturally supportslet andconst.Here we’ll elaborate more on these new declarations and why they’re preferable tovar.

If you’ve used JavaScript offhandedly, the next section might be a good way to refresh your memory.If you’re intimately familiar with all the quirks ofvar declarations in JavaScript, you might find it easier to skip ahead.

var declarations

Declaring a variable in JavaScript has always traditionally been done with thevar keyword.

ts
vara =10;

As you might’ve figured out, we just declared a variable nameda with the value10.

We can also declare a variable inside of a function:

ts
functionf() {
varmessage ="Hello, world!";
returnmessage;
}

and we can also access those same variables within other functions:

ts
functionf() {
vara =10;
returnfunctiong() {
varb =a +1;
returnb;
};
}
varg =f();
g();// returns '11'

In this above example,g captured the variablea declared inf.At any point thatg gets called, the value ofa will be tied to the value ofa inf.Even ifg is called oncef is done running, it will be able to access and modifya.

ts
functionf() {
vara =1;
a =2;
varb =g();
a =3;
returnb;
functiong() {
returna;
}
}
f();// returns '2'

Scoping rules

var declarations have some odd scoping rules for those used to other languages.Take the following example:

ts
functionf(shouldInitialize:boolean) {
if (shouldInitialize) {
varx =10;
}
returnx;
}
f(true);// returns '10'
f(false);// returns 'undefined'

Some readers might do a double-take at this example.The variablex was declaredwithin theif block, and yet we were able to access it from outside that block.That’s becausevar declarations are accessible anywhere within their containing function, module, namespace, or global scope - all which we’ll go over later on - regardless of the containing block.Some people call thisvar-scoping orfunction-scoping.Parameters are also function scoped.

These scoping rules can cause several types of mistakes.One problem they exacerbate is the fact that it is not an error to declare the same variable multiple times:

ts
functionsumMatrix(matrix:number[][]) {
varsum =0;
for (vari =0;i <matrix.length;i++) {
varcurrentRow =matrix[i];
for (vari =0;i <currentRow.length;i++) {
sum +=currentRow[i];
}
}
returnsum;
}

Maybe it was easy to spot out for some experienced JavaScript developers, but the innerfor-loop will accidentally overwrite the variablei becausei refers to the same function-scoped variable.As experienced developers know by now, similar sorts of bugs slip through code reviews and can be an endless source of frustration.

Variable capturing quirks

Take a quick second to guess what the output of the following snippet is:

ts
for (vari =0;i <10;i++) {
setTimeout(function () {
console.log(i);
},100 *i);
}

For those unfamiliar,setTimeout will try to execute a function after a certain number of milliseconds (though waiting for anything else to stop running).

Ready? Take a look:

10
10
10
10
10
10
10
10
10
10

Many JavaScript developers are intimately familiar with this behavior, but if you’re surprised, you’re certainly not alone.Most people expect the output to be

0
1
2
3
4
5
6
7
8
9

Remember what we mentioned earlier about variable capturing?Every function expression we pass tosetTimeout actually refers to the samei from the same scope.

Let’s take a minute to consider what that means.setTimeout will run a function after some number of milliseconds,but only after thefor loop has stopped executing;By the time thefor loop has stopped executing, the value ofi is10.So each time the given function gets called, it will print out10!

A common work around is to use an IIFE - an Immediately Invoked Function Expression - to capturei at each iteration:

ts
for (vari =0;i <10;i++) {
// capture the current state of 'i'
// by invoking a function with its current value
(function (i) {
setTimeout(function () {
console.log(i);
},100 *i);
})(i);
}

This odd-looking pattern is actually pretty common.Thei in the parameter list actually shadows thei declared in thefor loop, but since we named them the same, we didn’t have to modify the loop body too much.

let declarations

By now you’ve figured out thatvar has some problems, which is precisely whylet statements were introduced.Apart from the keyword used,let statements are written the same wayvar statements are.

ts
lethello ="Hello!";

The key difference is not in the syntax, but in the semantics, which we’ll now dive into.

Block-scoping

When a variable is declared usinglet, it uses what some calllexical-scoping orblock-scoping.Unlike variables declared withvar whose scopes leak out to their containing function, block-scoped variables are not visible outside of their nearest containing block orfor-loop.

ts
functionf(input:boolean) {
leta =100;
if (input) {
// Still okay to reference 'a'
letb =a +1;
returnb;
}
// Error: 'b' doesn't exist here
returnb;
}

Here, we have two local variablesa andb.a’s scope is limited to the body off whileb’s scope is limited to the containingif statement’s block.

Variables declared in acatch clause also have similar scoping rules.

ts
try {
throw"oh no!";
}catch (e) {
console.log("Oh well.");
}
// Error: 'e' doesn't exist here
console.log(e);

Another property of block-scoped variables is that they can’t be read or written to before they’re actually declared.While these variables are “present” throughout their scope, all points up until their declaration are part of theirtemporal dead zone.This is just a sophisticated way of saying you can’t access them before thelet statement, and luckily TypeScript will let you know that.

ts
a++;// illegal to use 'a' before it's declared;
leta;

Something to note is that you can stillcapture a block-scoped variable before it’s declared.The only catch is that it’s illegal to call that function before the declaration.If targeting ES2015, a modern runtime will throw an error; however, right now TypeScript is permissive and won’t report this as an error.

ts
functionfoo() {
// okay to capture 'a'
returna;
}
// illegal call 'foo' before 'a' is declared
// runtimes should throw an error here
foo();
leta;

For more information on temporal dead zones, see relevant content on theMozilla Developer Network.

Re-declarations and Shadowing

Withvar declarations, we mentioned that it didn’t matter how many times you declared your variables; you just got one.

ts
functionf(x) {
varx;
varx;
if (true) {
varx;
}
}

In the above example, all declarations ofx actually refer to thesamex, and this is perfectly valid.This often ends up being a source of bugs.Thankfully,let declarations are not as forgiving.

ts
letx =10;
letx =20;// error: can't re-declare 'x' in the same scope

The variables don’t necessarily need to both be block-scoped for TypeScript to tell us that there’s a problem.

ts
functionf(x) {
letx =100;// error: interferes with parameter declaration
}
functiong() {
letx =100;
varx =100;// error: can't have both declarations of 'x'
}

That’s not to say that a block-scoped variable can never be declared with a function-scoped variable.The block-scoped variable just needs to be declared within a distinctly different block.

ts
functionf(condition,x) {
if (condition) {
letx =100;
returnx;
}
returnx;
}
f(false,0);// returns '0'
f(true,0);// returns '100'

The act of introducing a new name in a more nested scope is calledshadowing.It is a bit of a double-edged sword in that it can introduce certain bugs on its own in the event of accidental shadowing, while also preventing certain bugs.For instance, imagine we had written our earliersumMatrix function usinglet variables.

ts
functionsumMatrix(matrix:number[][]) {
letsum =0;
for (leti =0;i <matrix.length;i++) {
varcurrentRow =matrix[i];
for (leti =0;i <currentRow.length;i++) {
sum +=currentRow[i];
}
}
returnsum;
}

This version of the loop will actually perform the summation correctly because the inner loop’si shadowsi from the outer loop.

Shadowing shouldusually be avoided in the interest of writing clearer code.While there are some scenarios where it may be fitting to take advantage of it, you should use your best judgement.

Block-scoped variable capturing

When we first touched on the idea of variable capturing withvar declaration, we briefly went into how variables act once captured.To give a better intuition of this, each time a scope is run, it creates an “environment” of variables.That environment and its captured variables can exist even after everything within its scope has finished executing.

ts
functiontheCityThatAlwaysSleeps() {
letgetCity;
if (true) {
letcity ="Seattle";
getCity =function () {
returncity;
};
}
returngetCity();
}

Because we’ve capturedcity from within its environment, we’re still able to access it despite the fact that theif block finished executing.

Recall that with our earliersetTimeout example, we ended up needing to use an IIFE to capture the state of a variable for every iteration of thefor loop.In effect, what we were doing was creating a new variable environment for our captured variables.That was a bit of a pain, but luckily, you’ll never have to do that again in TypeScript.

let declarations have drastically different behavior when declared as part of a loop.Rather than just introducing a new environment to the loop itself, these declarations sort of create a new scopeper iteration.Since this is what we were doing anyway with our IIFE, we can change our oldsetTimeout example to just use alet declaration.

ts
for (leti =0;i <10;i++) {
setTimeout(function () {
console.log(i);
},100 *i);
}

and as expected, this will print out

0
1
2
3
4
5
6
7
8
9

const declarations

const declarations are another way of declaring variables.

ts
constnumLivesForCat =9;

They are likelet declarations but, as their name implies, their value cannot be changed once they are bound.In other words, they have the same scoping rules aslet, but you can’t re-assign to them.

This should not be confused with the idea that the values they refer to areimmutable.

ts
constnumLivesForCat =9;
constkitty = {
name:"Aurora",
numLives:numLivesForCat,
};
// Error
kitty = {
name:"Danielle",
numLives:numLivesForCat,
};
// all "okay"
kitty.name ="Rory";
kitty.name ="Kitty";
kitty.name ="Cat";
kitty.numLives--;

Unless you take specific measures to avoid it, the internal state of aconst variable is still modifiable.Fortunately, TypeScript allows you to specify that members of an object arereadonly.Thechapter on Interfaces has the details.

let vs.const

Given that we have two types of declarations with similar scoping semantics, it’s natural to find ourselves asking which one to use.Like most broad questions, the answer is: it depends.

Applying theprinciple of least privilege, all declarations other than those you plan to modify should useconst.The rationale is that if a variable didn’t need to get written to, others working on the same codebase shouldn’t automatically be able to write to the object, and will need to consider whether they really need to reassign to the variable.Usingconst also makes code more predictable when reasoning about flow of data.

Use your best judgement, and if applicable, consult the matter with the rest of your team.

The majority of this handbook useslet declarations.

Destructuring

Another ECMAScript 2015 feature that TypeScript has is destructuring.For a complete reference, seethe article on the Mozilla Developer Network.In this section, we’ll give a short overview.

Array destructuring

The simplest form of destructuring is array destructuring assignment:

ts
letinput = [1,2];
let [first,second] =input;
console.log(first);// outputs 1
console.log(second);// outputs 2

This creates two new variables namedfirst andsecond.This is equivalent to using indexing, but is much more convenient:

ts
first =input[0];
second =input[1];

Destructuring works with already-declared variables as well:

ts
// swap variables
[first,second] = [second,first];

And with parameters to a function:

ts
functionf([first,second]: [number,number]) {
console.log(first);
console.log(second);
}
f([1,2]);

You can create a variable for the remaining items in a list using the syntax...:

ts
let [first, ...rest] = [1,2,3,4];
console.log(first);// outputs 1
console.log(rest);// outputs [ 2, 3, 4 ]

Of course, since this is JavaScript, you can just ignore trailing elements you don’t care about:

ts
let [first] = [1,2,3,4];
console.log(first);// outputs 1

Or other elements:

ts
let [,second, ,fourth] = [1,2,3,4];
console.log(second);// outputs 2
console.log(fourth);// outputs 4

Tuple destructuring

Tuples may be destructured like arrays; the destructuring variables get the types of the corresponding tuple elements:

ts
lettuple: [number,string,boolean] = [7,"hello",true];
let [a,b,c] =tuple;// a: number, b: string, c: boolean

It’s an error to destructure a tuple beyond the range of its elements:

ts
let [a,b,c,d] =tuple;// Error, no element at index 3

As with arrays, you can destructure the rest of the tuple with..., to get a shorter tuple:

ts
let [a, ...bc] =tuple;// bc: [string, boolean]
let [a,b,c, ...d] =tuple;// d: [], the empty tuple

Or ignore trailing elements, or other elements:

ts
let [a] =tuple;// a: number
let [,b] =tuple;// b: string

Object destructuring

You can also destructure objects:

ts
leto = {
a:"foo",
b:12,
c:"bar",
};
let {a,b } =o;

This creates new variablesa andb fromo.a ando.b.Notice that you can skipc if you don’t need it.

Like array destructuring, you can have assignment without declaration:

ts
({a,b } = {a:"baz",b:101 });

Notice that we had to surround this statement with parentheses.JavaScript normally parses a{ as the start of block.

You can create a variable for the remaining items in an object using the syntax...:

ts
let {a, ...passthrough } =o;
lettotal =passthrough.b +passthrough.c.length;

Property renaming

You can also give different names to properties:

ts
let {a:newName1,b:newName2 } =o;

Here the syntax starts to get confusing.You can reada: newName1 as ”a asnewName1”.The direction is left-to-right, as if you had written:

ts
letnewName1 =o.a;
letnewName2 =o.b;

Confusingly, the colon here doesnot indicate the type.The type, if you specify it, still needs to be written after the entire destructuring:

ts
let {a:newName1,b:newName2 }: {a:string;b:number } =o;

Default values

Default values let you specify a default value in case a property is undefined:

ts
functionkeepWholeObject(wholeObject: {a:string;b?:number }) {
let {a,b =1001 } =wholeObject;
}

In this example theb? indicates thatb is optional, so it may beundefined.keepWholeObject now has a variable forwholeObject as well as the propertiesa andb, even ifb is undefined.

Function declarations

Destructuring also works in function declarations.For simple cases this is straightforward:

ts
typeC = {a:string;b?:number };
functionf({a,b }:C):void {
// ...
}

But specifying defaults is more common for parameters, and getting defaults right with destructuring can be tricky.First of all, you need to remember to put the pattern before the default value.

ts
functionf({a ="",b =0 } = {}):void {
// ...
}
f();

The snippet above is an example of type inference, explained earlier in the handbook.

Then, you need to remember to give a default for optional properties on the destructured property instead of the main initializer.Remember thatC was defined withb optional:

ts
functionf({a,b =0 } = {a:"" }):void {
// ...
}
f({a:"yes" });// ok, default b = 0
f();// ok, default to { a: "" }, which then defaults b = 0
f({});// error, 'a' is required if you supply an argument

Use destructuring with care.As the previous example demonstrates, anything but the simplest destructuring expression is confusing.This is especially true with deeply nested destructuring, which getsreally hard to understand even without piling on renaming, default values, and type annotations.Try to keep destructuring expressions small and simple.You can always write the assignments that destructuring would generate yourself.

Spread

The spread operator is the opposite of destructuring.It allows you to spread an array into another array, or an object into another object.For example:

ts
letfirst = [1,2];
letsecond = [3,4];
letbothPlus = [0, ...first, ...second,5];

This gives bothPlus the value[0, 1, 2, 3, 4, 5].Spreading creates a shallow copy offirst andsecond.They are not changed by the spread.

You can also spread objects:

ts
letdefaults = {food:"spicy",price:"$$",ambiance:"noisy" };
letsearch = { ...defaults,food:"rich" };

Nowsearch is{ food: "rich", price: "$$", ambiance: "noisy" }.Object spreading is more complex than array spreading.Like array spreading, it proceeds from left-to-right, but the result is still an object.This means that properties that come later in the spread object overwrite properties that come earlier.So if we modify the previous example to spread at the end:

ts
letdefaults = {food:"spicy",price:"$$",ambiance:"noisy" };
letsearch = {food:"rich", ...defaults };

Then thefood property indefaults overwritesfood: "rich", which is not what we want in this case.

Object spread also has a couple of other surprising limits.First, it only includes an objects’own, enumerable properties.Basically, that means you lose methods when you spread instances of an object:

ts
classC {
p =12;
m() {}
}
letc =newC();
letclone = { ...c };
clone.p;// ok
clone.m();// error!

Second, the TypeScript compiler doesn’t allow spreads of type parameters from generic functions.That feature is expected in future versions of the language.

using declarations

using declarations are an upcoming feature for JavaScript that are part of theStage 3 Explicit Resource Management proposal. Ausing declaration is much like aconst declaration, except that it couples thelifetime of the value bound to thedeclaration with thescope of the variable.

When control exits the block containing ausing declaration, the[Symbol.dispose]() method of thedeclared value is executed, which allows that value to perform cleanup:

ts
functionf() {
usingx =newC();
doSomethingWith(x);
}// `x[Symbol.dispose]()` is called

At runtime, this has an effectroughly equivalent to the following:

ts
functionf() {
constx =newC();
try {
doSomethingWith(x);
}
finally {
x[Symbol.dispose]();
}
}

using declarations are extremely useful for avoiding memory leaks when working with JavaScript objects that hold on tonative references like file handles

ts
{
usingfile =awaitopenFile();
file.write(text);
doSomethingThatMayThrow();
}// `file` is disposed, even if an error is thrown

or scoped operations like tracing

ts
functionf() {
usingactivity =newTraceActivity("f");// traces entry into function
// ...
}// traces exit of function

Unlikevar,let, andconst,using declarations do not support destructuring.

null andundefined

It’s important to note that the value can benull orundefined, in which case nothing is disposed at the end of theblock:

ts
{
usingx =b ?newC() :null;
// ...
}

which isroughly equivalent to:

ts
{
constx =b ?newC() :null;
try {
// ...
}
finally {
x?.[Symbol.dispose]();
}
}

This allows you to conditionally acquire resources when declaring ausing declaration without the need for complexbranching or repetition.

Defining a disposable resource

You can indicate the classes or objects you produce are disposable by implementing theDisposable interface:

ts
// from the default lib:
interfaceDisposable {
[Symbol.dispose]():void;
}
// usage:
classTraceActivityimplementsDisposable {
readonlyname:string;
constructor(name:string) {
this.name =name;
console.log(`Entering:${name}`);
}
[Symbol.dispose]():void {
console.log(`Exiting:${name}`);
}
}
functionf() {
using_activity =newTraceActivity("f");
console.log("Hello world!");
}
f();
// prints:
// Entering: f
// Hello world!
// Exiting: f

await using declarations

Some resources or operations may have cleanup that needs to be performed asynchronously. To accommodate this, theExplicit Resource Management proposal also introducestheawait using declaration:

ts
asyncfunctionf() {
awaitusingx =newC();
}// `await x[Symbol.asyncDispose]()` is invoked

Anawait using declaration invokes, andawaits, its value’s[Symbol.asyncDispose]() method as control leaves thecontaining block. This allows for asynchronous cleanup, such as a database transaction performing a rollback or commit,or a file stream flushing any pending writes to storage before it is closed.

As withawait,await using can only be used in anasync function or method, or at the top level of a module.

Defining an asynchronously disposable resource

Just asusing relies on objects that areDisposable, anawait using relies on objects that areAsyncDisposable:

ts
// from the default lib:
interfaceAsyncDisposable {
[Symbol.asyncDispose]:PromiseLike<void>;
}
// usage:
classDatabaseTransactionimplementsAsyncDisposable {
publicsuccess =false;
privatedb:Database |undefined;
privateconstructor(db:Database) {
this.db =db;
}
staticasynccreate(db:Database) {
awaitdb.execAsync("BEGIN TRANSACTION");
returnnewDatabaseTransaction(db);
}
async [Symbol.asyncDispose]() {
if (this.db) {
constdb =this.db:
this.db =undefined;
if (this.success) {
awaitdb.execAsync("COMMIT TRANSACTION");
}
else {
awaitdb.execAsync("ROLLBACK TRANSACTION");
}
}
}
}
asyncfunctiontransfer(db:Database,account1:Account,account2:Account,amount:number) {
usingtx =awaitDatabaseTransaction.create(db);
if (awaitdebitAccount(db,account1,amount)) {
awaitcreditAccount(db,account2,amount);
}
// if an exception is thrown before this line, the transaction will roll back
tx.success =true;
// now the transaction will commit
}

await using vsawait

Theawait keyword that is part of theawait using declaration only indicates that thedisposal of the resource isawait-ed. It doesnotawait the value itself:

ts
{
awaitusingx =getResourceSynchronously();
}// performs `await x[Symbol.asyncDispose]()`
{
awaitusingy =awaitgetResourceAsynchronously();
}// performs `await y[Symbol.asyncDispose]()`

await using andreturn

It’s important to note that there is a small caveat with this behavior if you are using anawait using declaration inanasync function that returns aPromise without firstawait-ing it:

ts
functiong() {
returnPromise.reject("error!");
}
asyncfunctionf() {
awaitusingx =newC();
returng();// missing an `await`
}

Because the returned promise isn’tawait-ed, it’s possible that the JavaScript runtime may report an unhandledrejection since execution pauses whileawait-ing the asynchronous disposal ofx, without having subscribed to thereturned promise. This is not a problem that is unique toawait using, however, as this can also occur in anasyncfunction that usestry..finally:

ts
asyncfunctionf() {
try {
returng();// also reports an unhandled rejection
}
finally {
awaitsomethingElse();
}
}

To avoid this situation, it is recommended that youawait your return value if it may be aPromise:

ts
asyncfunctionf() {
awaitusingx =newC();
returnawaitg();
}

using andawait using infor andfor..of statements

Bothusing andawait using can be used in afor statement:

ts
for (usingx =getReader(); !x.eof;x.next()) {
// ...
}

In this case, the lifetime ofx is scoped to the entirefor statement and is only disposed when control leaves theloop due tobreak,return,throw, or when the loop condition is false.

In addition tofor statements, both declarations can also be used infor..of statements:

ts
function*g() {
yieldcreateResource1();
yieldcreateResource2();
}
for (usingxofg()) {
// ...
}

Here,x is disposed at the end ofeach iteration of the loop, and is then reinitialized with the next value. This isespecially useful when consuming resources produced one at a time by a generator.

using andawait using in older runtimes

using andawait using declarations can be used when targeting older ECMAScript editions as long as you are usinga compatible polyfill forSymbol.dispose/Symbol.asyncDispose, such as the one provided by default in recenteditions of NodeJS.

The TypeScript docs are an open source project. Help us improve these pagesby sending a Pull Request

Contributors to this page:
DRDaniel Rosenwasser  (58)
OTOrta Therox  (20)
NSNathan Shively-Sanders  (9)
VRVimal Raghubir  (3)
BCBrett Cannon  (3)
24+

Last updated: Dec 16, 2025