Defining variables
Key points for defining variables
Good variable definitions improve the readability and maintainability of your code.
- Avoid magic numbers
- Keep shared values in constants
- Don't give a variable multiple meanings
- Minimize the scope of a variable
- Prefer const
- Specify appropriate types
Avoid magic numbers and magic characters
A magic number is a number written directly in code whose meaning is unclear.
❌ Bad: Magic numbers
// ❌ It's unclear what 7 means
function getWeekdayName(index: number): string {
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return days[index % 7];
}
// ❌ It's unclear what 86400 means
function convertDaysToSeconds(days: number): number {
return days * 86400;
}
// ❌ It's unclear what 0, 1, 2 represent
if (user.status === 0) {
console.log('Active');
} else if (user.status === 1) {
console.log('Suspended');
} else if (user.status === 2) {
console.log('Deleted');
}
// ❌ It's unclear what 1.1 means
const totalPrice = price * 1.1;
✅ Good: Use named constants
// ✅ Clarify the meaning with a constant
const DAYS_IN_WEEK = 7;
function getWeekdayName(index: number): string {
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return days[index % DAYS_IN_WEEK];
}
// ✅ The formula is easy to understand
const HOURS_PER_DAY = 24;
const MINUTES_PER_HOUR = 60;
const SECONDS_PER_MINUTE = 60;
const SECONDS_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE;
function convertDaysToSeconds(days: number): number {
return days * SECONDS_PER_DAY;
}
// ✅ Clarify the states with an enum
enum UserStatus {
Active = 0,
Suspended = 1,
Deleted = 2,
}
if (user.status === UserStatus.Active) {
console.log('Active');
} else if (user.status === UserStatus.Suspended) {
console.log('Suspended');
} else if (user.status === UserStatus.Deleted) {
console.log('Deleted');
}
// ✅ Turn the tax rate into a constant
const TAX_RATE = 0.1;
const totalPrice = price * (1 + TAX_RATE);
An enum can also clarify the meaning of states, but for new code, an as const object + union literal type is recommended. For details, see Union types, literal types, and enum.
Exception: cases where magic numbers are acceptable
// ✅ Self-evident values like the following don't need constants
// Array initialization
const emptyArray: number[] = [];
const firstItem = items[0];
// Comparison
if (count === 0) { // ✅ 0 is self-evident
console.log('It is empty');
}
// Mathematical constants
const double = value * 2; // ✅ Doubling is self-evident
const half = value / 2; // ✅ Halving is self-evident
// Loops
for (let i = 0; i < 10; i++) { // ✅ A small loop range is acceptable
// ...
}
// But turn it into a constant when it has meaning
const MAX_RETRY_COUNT = 10;
for (let i = 0; i < MAX_RETRY_COUNT; i++) {
// ...
}
Keep shared values in constants
Define values used throughout the project in one place and reuse them.
❌ Bad: Values are scattered
// ❌ The same value in multiple places
// file1.ts
if (age >= 20) {
// ...
}
// file2.ts
function canVote(age: number): boolean {
return age >= 20;
}
// file3.ts
const isAdult = user.age >= 20;
Problems:
- When changing the value, you must fix every place
- It's easy to miss a spot
✅ Good: Manage centrally with a constant
// constants.ts
export const ADULT_AGE = 20;
export const TAX_RATE = 0.1;
export const MAX_FILE_SIZE_MB = 10;
export const API_BASE_URL = 'https://api.example.com';
// file1.ts
import { ADULT_AGE } from './constants';
if (age >= ADULT_AGE) {
// ...
}
// file2.ts
import { ADULT_AGE } from './constants';
function canVote(age: number): boolean {
return age >= ADULT_AGE;
}
// file3.ts
import { ADULT_AGE } from './constants';
const isAdult = user.age >= ADULT_AGE;
Benefits:
- Change the value in only one place
- The meaning of the value is clear
- Prevents typos
Don't give a variable information beyond its intended purpose
❌ Bad: A variable with multiple meanings
// ❌ Bad: result has multiple meanings
// -1: error, 0 or greater: the index on success
function findUser(name: string): number {
const index = users.findIndex(u => u.name === name);
if (index === -1) {
return -1; // Error
}
return index; // Success
}
const result = findUser('Taro');
if (result === -1) {
console.log('User not found');
} else {
console.log(`Index: ${result}`);
}
Problems:
- Uses the special value
-1to express an error - The type alone doesn't reveal success/failure
✅ Good: Express it with an explicit type
// ✅ Good: Distinguish success and failure with a type
type FindResult =
| { success: true; index: number }
| { success: false; error: string };
function findUser(name: string): FindResult {
const index = users.findIndex(u => u.name === name);
if (index === -1) {
return { success: false, error: 'User not found' };
}
return { success: true, index };
}
const result = findUser('Taro');
if (result.success) {
console.log(`Index: ${result.index}`);
} else {
console.log(result.error);
}
// ✅ Or use undefined
function findUserIndex(name: string): number | undefined {
const index = users.findIndex(u => u.name === name);
return index !== -1 ? index : undefined;
}
const index = findUserIndex('Taro');
if (index !== undefined) {
console.log(`Index: ${index}`);
} else {
console.log('User not found');
}
Make values explicit with destructuring
Rather than indexed access into an array, make the meaning clear with destructuring.
❌ Bad: Indexed access
// ❌ It's unclear what [0], [1] mean
const data = getUserData();
const name = data[0];
const age = data[1];
const email = data[2];
// ❌ The result of split is hard to read
const parts = '2024-01-15'.split('-');
const year = parts[0];
const month = parts[1];
const day = parts[2];
✅ Good: Destructuring
// ✅ The meaning is clear with destructuring
const [name, age, email] = getUserData();
// ✅ Even clearer
const [year, month, day] = '2024-01-15'.split('-');
// ✅ For objects
const { name, age, email } = user;
// ✅ Getting only part of an array
const [first, second, ...rest] = items;
const [, , third] = items; // Only the third
// ✅ Default values
const [name = 'Unknown', age = 0] = getUserData();
Define one variable for one purpose
❌ Bad: Reusing a variable
// ❌ Reusing temp for multiple purposes
let temp = user.firstName + ' ' + user.lastName;
console.log(temp);
temp = temp.toUpperCase();
console.log(temp);
temp = calculateAge(user.birthDate);
console.log(temp); // It's unclear that temp now represents the age
Problems:
- What
temprepresents keeps changing - Hard to debug
- The intent is unclear
✅ Good: A dedicated variable for each purpose
// ✅ Each variable has a clear meaning
const fullName = user.firstName + ' ' + user.lastName;
console.log(fullName);
const fullNameUpper = fullName.toUpperCase();
console.log(fullNameUpper);
const age = calculateAge(user.birthDate);
console.log(age);
❌ Bad: Reusing a variable
// ❌ Reusing result
function processData(input: string): number {
let result = input.trim();
result = result.toLowerCase();
result = result.replace(/\s+/g, '');
result = result.length; // The type changes!
return result;
}
Note that this code becomes a TS2322 compile error in TypeScript (you can't assign a number to result, which is inferred as string). TypeScript structurally prevents reuse that changes the type, but reuse with the same type (the earlier temp example) is not detected, so as a rule, follow "one variable, one purpose."
✅ Good: A new variable at each step
// ✅ Each step is clear
function processData(input: string): number {
const trimmed = input.trim();
const lowercase = trimmed.toLowerCase();
const withoutSpaces = lowercase.replace(/\s+/g, '');
const length = withoutSpaces.length;
return length;
}
// ✅ Or method chaining
function processData(input: string): number {
return input
.trim()
.toLowerCase()
.replace(/\s+/g, '')
.length;
}
Define objects and arrays with const
What const means in TypeScript
// const means "cannot be reassigned" (not immutable)
const user = { name: 'Taro' };
user.name = 'Hanako'; // ✅ Changing a property is allowed
// user = { name: 'Jiro' }; // ❌ Reassignment is not allowed
const items = [1, 2, 3];
items.push(4); // ✅ Adding an element is allowed
// items = [5, 6]; // ❌ Reassignment is not allowed
❌ Bad: Using let
// ❌ Using let even though there's no reassignment
let user = { name: 'Taro' };
let items = [1, 2, 3];
// It's unclear whether it will be reassigned later or whether a property will change
✅ Good: Using const
// ✅ Clear that it won't be reassigned
const user = { name: 'Taro' };
const items = [1, 2, 3];
user.name = 'Hanako'; // Changing a property is allowed
items.push(4); // Adding an element is allowed
When you want it fully immutable
// ✅ Immutable at the type level with as const (compile-time check)
const user = { name: 'Taro' } as const;
// user.name = 'Hanako'; // ❌ Error
const items = [1, 2, 3] as const;
// items.push(4); // ❌ Error
// ✅ Immutable at runtime with Object.freeze
const user2 = Object.freeze({ name: 'Taro' });
// user2.name = 'Hanako'; // ❌ Compile error (TS2540). Even if you bypass the type, it's a runtime TypeError in strict mode
// ✅ ReadonlyArray
const items2: readonly number[] = [1, 2, 3];
// items2.push(4); // ❌ Compile error
Keep variable lifetimes (scope) short
When a variable's scope is wide, the following problems occur:
- Hard to track where the value changes
- Unintended reassignment happens easily
- Name collisions occur easily
- Memory is not released easily
❌ Bad: A wide scope
// ❌ A variable used throughout the function
function processUsers(): void {
let user: User; // The scope is too wide
let result: string;
for (let i = 0; i < users.length; i++) {
user = users[i];
if (user.isActive) {
result = `${user.name} is active`;
console.log(result);
}
}
// user and result can be used unintentionally
console.log(user); // The last user
}
✅ Good: Minimize the scope
// ✅ Define it where it's needed
function processUsers(): void {
for (let i = 0; i < users.length; i++) {
const user = users[i]; // Valid only inside the loop
if (user.isActive) {
const result = `${user.name} is active`; // Valid only inside the if block
console.log(result);
}
}
}
// ✅ Better: use forEach
function processUsers(): void {
users.forEach(user => {
if (user.isActive) {
const result = `${user.name} is active`;
console.log(result);
}
});
}
Scope in a for loop
// ❌ Bad: Defining the variable before the loop
let i: number;
for (i = 0; i < 10; i++) {
console.log(i);
}
console.log(i); // Usable outside the loop (unintended)
// ✅ Good: Valid only inside the loop
for (let i = 0; i < 10; i++) {
console.log(i);
}
// console.log(i); // ❌ Error (out of scope)
Avoid lexical scope as much as possible
Lexical scope is the mechanism by which a variable's scope is determined by its declaration position in the source code. As a result, an inner function (a closure) can reference and change variables of the outer function. What you want to avoid here is the pattern of using lexical scope to implicitly depend on an outer mutable variable.
❌ Bad: Depending on an outer variable
// ❌ Depending on an outer variable
function createCounter() {
let count = 0; // The outer variable
function increment() {
count++; // Changes the outer count
console.log(count);
}
function decrement() {
count--; // Changes the outer count
console.log(count);
}
function reset() {
count = 0; // Changes the outer count
}
return { increment, decrement, reset };
}
Problems:
- Hard to track where
countis changed - Difficult to test
✅ Good: Make state management explicit with a class
// ✅ Manage state with a class
class Counter {
private count = 0;
increment(): void {
this.count++;
console.log(this.count);
}
decrement(): void {
this.count--;
console.log(this.count);
}
reset(): void {
this.count = 0;
}
getCount(): number {
return this.count;
}
}
With a class, the places that change this.count are confined within the class definition, and the state and the methods that change it gather in one place. Because where the state lives and how it changes are made explicit, changes are easy to track and the code is easy to test.
Don't use the global scope
Because global variables can be referenced and changed from the entire program, they have the following problems:
- It's unclear where they're changed
- Name collisions occur easily
- Difficult to test
- Hard to modularize
❌ Bad: Global variables
// ❌ Global variables
let currentUser: User | null = null;
let isLoggedIn = false;
function login(user: User) {
currentUser = user;
isLoggedIn = true;
}
function logout() {
currentUser = null;
isLoggedIn = false;
}
✅ Good: Module scope or a class
// ✅ Module scope (accessible only within the file)
// auth.ts
let currentUser: User | null = null;
let isLoggedIn = false;
export function login(user: User): void {
currentUser = user;
isLoggedIn = true;
}
export function logout(): void {
currentUser = null;
isLoggedIn = false;
}
export function getCurrentUser(): User | null {
return currentUser;
}
export function checkIsLoggedIn(): boolean {
return isLoggedIn;
}
// ✅ Even better: manage state with a class
export class AuthService {
private currentUser: User | null = null;
login(user: User): void {
this.currentUser = user;
}
logout(): void {
this.currentUser = null;
}
getCurrentUser(): User | null {
return this.currentUser;
}
isLoggedIn(): boolean {
return this.currentUser !== null;
}
}
Don't use the same variable name in different scopes
❌ Bad: Shadowing (variable hiding)
// ❌ Variables with the same name are nested
const name = 'Global';
function greet() {
const name = 'Function'; // Hides the outer name
console.log(name); // "Function"
if (true) {
const name = 'Block'; // Hides it further
console.log(name); // "Block"
}
console.log(name); // "Function"
}
console.log(name); // "Global"
Problems:
- It's unclear which
nameis being referenced - It's easy to introduce bugs when refactoring
✅ Good: Use different names
// ✅ Clear with different names
const globalName = 'Global';
function greet() {
const functionName = 'Function';
console.log(functionName);
if (true) {
const blockName = 'Block';
console.log(blockName);
}
console.log(functionName);
}
console.log(globalName);
Exercise 7: Defining variables
Improve the following code.
// ❌ Before
function calculatePrice(items: any[]) {
let total = 0;
let i;
for (i = 0; i < items.length; i++) {
total = total + items[i].price;
}
// Add consumption tax
total = total * 1.1;
// Add shipping fee
if (total < 5000) {
total = total + 500;
}
return total;
}
let userName = 'Taro';
let userAge = 25;
function updateUser() {
userName = 'Hanako';
userAge = 30;
}
📝 View answer
// ✅ After
// Define constants
const TAX_RATE = 0.1;
const FREE_SHIPPING_THRESHOLD = 5000;
const SHIPPING_FEE = 500;
interface Item {
price: number;
}
function calculatePrice(items: Item[]): number {
// Calculate the subtotal
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
// Price including tax
const priceWithTax = subtotal * (1 + TAX_RATE);
// Shipping fee
const shippingFee = priceWithTax < FREE_SHIPPING_THRESHOLD ? SHIPPING_FEE : 0;
// Total amount
const totalPrice = priceWithTax + shippingFee;
return totalPrice;
}
// ✅ Avoid global variables, manage with a class
class User {
constructor(
private name: string,
private age: number
) {}
updateUser(name: string, age: number): void {
this.name = name;
this.age = age;
}
getName(): string {
return this.name;
}
getAge(): number {
return this.age;
}
}
const user = new User('Taro', 25);
user.updateUser('Hanako', 30);
Key improvements:
- Turn magic numbers into constants (TAX_RATE, SHIPPING_FEE, and so on)
- Minimize the scope of the loop variable
- Use meaningful variable names for each calculation step
- Manage global variables with a class
- Make types explicit (the Item interface)
Summary of defining variables
- Turn magic numbers into constants
- Define shared values in one place
- One purpose per variable
- Prefer const
- Minimize scope
- Avoid global variables
- Make meaning clear with destructuring
- Make types explicit
What to read next
- Function design — how to divide the responsibilities of functions that handle variables
- Naming conventions — return to the principles for naming variables