TypeScript Variable Declaration

By building upon the foundations of JavaScript, TypeScript introduces a robust static type system that makes variable declaration significantly more rigorous and predictable. While JavaScript developers are accustomed to the flexibility of dynamic typing, TypeScript provides the tools necessary to catch errors during development rather than at runtime.

Understanding how to declare variables effectively is the first step toward mastering TypeScript.
In TypeScript, you have three ways to declare a variable, each with distinct behaviors regarding scope, hoisting, and reassignability.

var: The Legacy Approach

var is the traditional way to declare variables in JavaScript. It is function-scoped, meaning a variable declared with var is available throughout the entire function in which it is defined. Furthermore, var is subject to hoisting, where the declaration is moved to the top of its scope during execution, often resulting in variables being initialized as undefined.

Because var also allows for re-declaration within the same scope, it can lead to accidental variable overwrites. Consequently, in modern TypeScript development, var is almost universally discouraged.

let: The Modern Standard for Reassignable Variables

let was introduced to solve the pitfalls of var. It is block-scoped, meaning the variable only exists within the nearest pair of curly braces {} (such as in an if statement or a for loop). Unlike var, let does not allow you to re-declare the same variable within the same scope, providing much better protection against bugs.

const: For Immutable Bindings

const is also block-scoped, but it carries an additional constraint: once a value is assigned to a const variable, it cannot be reassigned. This makes const the ideal choice for values that should remain constant throughout the lifecycle of your application.

var a = 10;
let b = 20;
const c = 30;

// var allows re-declaration (not recommended)
var a = 40; // Valid

// let and const prevent re-declaration
// let b = 50; // Error: Cannot redeclare block-scoped variable 'b'
// const c = 60; // Error: Cannot redeclare block-scoped variable 'c'

Type Annotations and Type Inference

One of TypeScript's most powerful features is its ability to manage types. You can handle types in two ways: explicitly or implicitly.

Explicit Type Annotations

When you want to be absolutely certain about the type of a variable, you can use type annotations. This is particularly useful when a variable's type isn't immediately obvious from its initial value.

let count: number = 10;          // Explicitly typed as a number
let message: string = "Hello";   // Explicitly typed as a string
let values: number[] = [1, 2, 3]; // Explicitly typed as an array of numbers

Type Inference

TypeScript is also smart enough to guess the type of a variable based on its assigned value. This is known as type inference. Using inference can make your code cleaner and less verbose without sacrificing safety.

let message = "Hello";           // TypeScript infers this is a string
let isActive = true;             // TypeScript infers this is a boolean

Union Types

Sometimes, a variable might need to hold more than one type of data. In such cases, you can use Union Types using the pipe (|) symbol.

let userId: string | number;
userId = "abc"; // Valid
userId = 123;   // Also valid

Scope and the Temporal Dead Zone (TDZ)

Understanding how variables are accessed within different code structures is crucial for avoiding runtime errors.

Block Scope vs. Function Scope

Because let and const are block-scoped, they are confined to the specific block in which they are declared. This prevents "leaking" variables into the outer scope, a common issue with var.

function scopeExample() {
  if (true) {
    var x = 10;
    let y = 20;
    const z = 30;
  }
  console.log(x); // 10 (var is hoisted to the function scope)
  // console.log(y); // Error: y is not defined in this scope
  // console.log(z); // Error: z is not defined in this scope
}

The Temporal Dead Zone (TDZ)

While let and const are technically hoisted, they are not initialized. The period between the start of the block and the actual declaration of the variable is known as the Temporal Dead Zone (TDZ). Attempting to access a variable within this zone will result in a runtime error.

console.log(a); // undefined (due to var hoisting)
var a = 1;

// console.log(b); // Error: Cannot access 'b' before initialization
let b = 2;

The TDZ is a safety mechanism that forces developers to declare variables before using them, leading to more predictable code execution.

The Nuances of const Immutability

A common misconception is that const makes a value completely immutable. In reality, const creates an immutable binding. This means you cannot point the variable to a new memory address (reassignment), but if the value is an object or an array, you can still modify its internal properties.

const person = { name: "Alice", age: 25 };
person.age = 26; // Valid: You are mutating a property, not reassigning the variable

// person = { name: "Bob" }; // Error: Cannot reassign a constant variable

const numbers = [1, 2, 3];
numbers.push(4); // Valid: Mutating the array
// numbers = [5, 6]; // Error: Cannot reassign

To achieve true immutability for objects, TypeScript provides the as const assertion (known as const assertions), which makes every property of the object readonly.

const config = {
  apiUrl: "https://api.example.com",
  timeout: 5000
} as const;

// config.timeout = 10000; // Error: Cannot assign to 'timeout' because it is a read-only property

Destructuring Declarations

TypeScript fully supports modern JavaScript destructuring patterns, allowing you to extract values from arrays or objects into distinct variables with full type safety.

// Array destructuring with explicit types
const [first, second]: number[] = [1, 2];

// Object destructuring with explicit types
const user = { name: "Bob", age: 30 };
const { name, age }: { name: string; age: number } = user;

// Destructuring with default values
const { name: userName = "Guest", age: userAge = 0 } = user;

Destructuring makes your code more concise and readable, especially when dealing with complex data structures returned from APIs.

Best Practices Summary

To write clean, professional, and type-safe TypeScript, follow these industry-standard guidelines:

  1. Default to const: Always use const for every declaration unless you know for a fact the variable needs to be reassigned. This signals intent and prevents accidental changes.
  2. Use let for reassignments: If a variable must change (e.g., a counter in a loop), use let.
  3. Avoid var entirely: There is rarely a legitimate reason to use var in modern TypeScript development.
  4. Balance Inference and Annotation: Rely on type inference for simple, obvious assignments to keep code clean. However, use explicit annotations for function parameters, complex object structures, and union types to ensure clarity.
  5. Respect the TDZ: Always declare your variables at the top of their respective scopes to avoid Temporal Dead Zone errors.
  6. Leverage as const: When defining configuration objects or fixed sets of data, use as const to ensure deep immutability.