Reassigning variables is like changing the past. When we see code like this:
We can’t be sure that our pizza will always have salami on it because this variable is defined using the let keyword, so we can assign it a new value:
No salami anymore!
We can even assign it a value of another type:
Knowing that this is possible makes us wonder, every time we see pizzaTopping in the code, which value it has now. That’s a huge and unnecessary cognitive load that we should avoid.
We can declare a variable using the const keyword to disallow reassignments:
This pizza will always have salami on it!
Info The const and let keywords are relatively new in JavaScript and were introduced in ECMAScript 2015. Before that, JavaScript only had the var keyword, which is no longer recommended. The main difference is that const and let are block-scoped (meaning, the variable is accessible inside a single block, such as an if condition or a for loop), while var is function-scoped (meaning, the variable is accessible anywhere within in a function). Additionally, we can now choose whether to allow reassignments or not, depending on which keyword we use to declare a variable.
Most of the time, we can write code without reassignments, making it easier to reason about.
Info Arrays and objects can also be mutated, even if they are defined using the const keyword. We talk about mutation in the next chapter, Avoid mutation.
Sometimes, a variable is reused to store different values:
In the code above, the category variable can contain: a category ID (a number or a string), a list of products in a category (an array), and a list of filtered products (also an array). This function isn’t completely hopeless because it’s short, but imagine more code between each reassignment.
On top of that, a new value is reassigned to a function parameter, which is known as function parameter shadowing. I think it’s no different from regular reassignment since it only affects the value inside the function, so I’ll treat it the same way.
InfoVariable shadowing happens when we define a variable with the same name that already exists in a larger scope. For example, we define a text variable inside a function, but there’s already a module-level text variable. Shadowing makes it hard to know which variable we’re working with at any given moment. Info: Info: Function parameter shadowing is similar, but happens when we reassign a function parameter.
Such cases are the easiest to fix: we need to use separate variables for each value:
By doing this, we shortened the lifespan of each variable and gave them clearer names, so the code is easier to understand, and we need to read less code to find out the current (and now the only) values of each variable. Also, each variable can have its own type that doesn’t change over its lifespan:
InfoVariable lifespan is the number of lines of code between the variable declaration and the last line where this variable is accessed. The longer the lifespan, the harder it is to follow a variable and know which value it has at any given moment.
This approach makes it significantly easier to reason about the code.
Variables that allow different types are awkward in TypeScript. I think it’s a clear sign that we’re doing something wrong:
These extra types and conditions aren’t just cumbersome, they prevent TypeScript from doing its job: now the type of the getProductsOnSale() function says that we can call it with a product array, which doesn’t make sense.
The most common use case for reassignment is probably incremental computations. Consider this example:
This code validates a video file upload by checking that it has all required data in the correct formats. I’ve shortened the comments a bit because the original code had lines longer than 200 characters. On a very big screen, it looks like a pretty table; otherwise, it’s an unreadable mess. Any autoformatting tool, like Prettier, will also turn this code into an unreadable mess. Such formatting handcraft was common in the old days, before autoformatting tools became mainstream. Now, it’s a waste of time.
Info We talk about code formatting and Prettier in the Autoformat your code chapter.
Anyway, this code appends an error message to the errors string variable for every failed validation, but now it’s hard to see this because the message formatting code is intertwined with the validation code. To add another validation, we have to understand and copy the formatting code. To print errors as an HTML list instead of plain text, we have to change each line of this function.
Let’s separate the validation from the formatting:
We’ve separated the validations, the validation logic, and the formatting. Flies separately, kebabs separately, as we say in Russia. Each piece of code now has a single responsibility and a single reason to change. The validations are now defined declaratively and read like a list, not mixed with conditions and string concatenation. We’ve also changed negative conditions (is invalid?) to positive ones (is valid?). All this improves the readability and maintainability of the code: it’s easier to see all validations and add new ones because we don’t need to know the implementation details of running validations or formatting.
Info We talk more about negative and positive names in the Naming is hard chapter.
On top of that, now it’s clear that the original code had a bug: there was no space between error messages.
Finally, we can now swap the formatting function and render errors as an HTML list, for example:
We can also test each validation separately. Have you noticed that I’ve changed false to null in the last validation? That’s because match() returns null when there’s no match, not false. The original validation always returns true.
I would even inline ERROR_MESSAGES constants unless they are reused somewhere else. They don’t make the code easier to read, but they make it harder to change because we have change the code in two places.
Now, all the code we need to touch to add, remove, or change validations is contained in the VIDEO_VALIDATIONS array. Keep the code, that’s likely to be changed at the same time, in the same place.
Another common reason for reassignments is building complex objects:
In the code above, we add the from and to properties only when they aren’t empty.
The code would be clearer if we taught our backend to ignore undefined values and build the whole object at once:
Now, the query object always has the same shape, but some properties can be undefined. The code feels more declarative, and it’s easier to see the final shape of this object.
Some programmers like to define all variables at the beginning of a function. I call this Pascal style because, in the Pascal programming language, we have to declare all variables at the beginning of a program or a function:
Some programmers use this style in languages that don’t require it:
A long variable lifespan requires a lot of scrolling to understand the current value of the variable. Possible reassignments make it even worse. If there are 50 lines between a variable declaration and its usage, the variable can be reassigned at any of these 50 lines.
We can improve readability by moving variable declarations as close to their usage as possible and by avoiding reassignments:
We shortened the isFreeDelivery variable lifespan from 100 lines to just 10. Now, it’s also clear that its value is the one we assign when we declare it.
Another change here is that the isFreeDelivery variable is now boolean, which makes the code more idiomatic. We convert it to a number (a backend requirement) when we submit the order to the server. This is similar to the normalization of variables we talked about in the previous chapter; the only difference is that in the previous chapter we normalized the incoming variable, while here we normalize the outgoing one. The structure of the raw data and the requirements of the backend shouldn’t make us write confusing and non-idiomatic code.
Tip Don’t confuse the Pascal-style with PascalCase though, this naming convention is still in use. We talk more about naming conventions in the Naming is hard chapter.
#Avoid temporary variables for function return values
When a variable is used to store the result of a function, we can often get rid of that variable:
In the code above, we check that every event is valid, which would be clearer with the every() array method:
Info We talk a lot more about array methods in the Avoid loops chapter.
We removed a temporary variable, avoided reassignments, and made the condition positive (is valid?) instead of negative (is invalid?). Positive conditions are generally easier to understand.
Let’s have a look at another example that returns a value if it’s present and zero otherwise:
We can use a ternary operator to avoid reassignment:
Now, the flow of the code is more straightforward.
Sometimes, attempting to avoid reassignments makes the code harder to read. In such cases, we can either leave the code as is or extract the code into a function and use the function’s return value instead of a reassignment.
Consider this example:
This code is okay. The function isn’t too long to worry much about reassignments or pretty much anything. However, for the sake of example (and because a more realistic example would make it much larger), let’s extract the error type logic into a function:
I often call this method sweeping under the rug refactoring: we stash a complex or long piece of code into its own function, making it easier to understand the main function.
However, we should avoid splitting code into many small functions: doing so significantly hurts readability because it’s hard to understand the code when useful pieces are hidden in other functions. All good things are best in moderation.
For this particular code, I’d prefer to use a table instead of a function.
Sometimes, having a reassignment is quite okay. Indeterminate loops, where we don’t know the number of iterations up front, are a good case for reassignments.
Consider this example:
In the code above, we find the start of the current week by moving one day back in a while loop and checking if it’s already Monday or not.
Even if it’s possible to avoid reassignments here, it will likely make the code less readable. Feel free to try, and let me know how it goes, though.
Many practices I discourage in this book make TypeScript code awkward and convoluted. Reassignments are no exception. Consider this example:
TypeScript has great type inference, meaning we don’t have to explicitly define types all the time. Often, TypeScript can determine the correct type based on, for example, the value or the function return type. Here, though, we don’t take advantage of type inference: we must specify a type for the hideTimeout variable because it has no initial value.
Let’s rewrite this code without reassignments:
Now, we rely on type inference: TypeScript knows that the setTimeout() function returns a NodeJS.Timeout, so it can safely assume that hideTimeout should use the same type. Additionally, by restructuring the code, we removed the second condition: now we return a no operation function when there’s no need for a timer. This makes the code less cluttered and easier to follow.
Info A no operation or noop function is a function that does nothing. It allows us to write unconditional code that expects a function: if we don’t want any action, we pass a no operation function instead. This makes the code more straightforward. Noop functions are a common pattern in JavaScript, and arrow function syntax gives them a compact and distinctive look: () => {}.
We already discussed another example of how reassignments make TypeScript code awkward in the Don’t reuse variables section.
In all examples above, we replaced let with const in variable declarations. This immediately tells the reader that the variable won’t be reassigned. We can be sure it won’t: the compiler will yell at us if we try. Every time we see a let declaration, we know that this code is likely more complex and that we should read it more carefully.
Another useful convention is to use SCREAMING_SNAKE_CASE names for constants. This tells the reader that it’s more of a configuration value than a result of some computation. The lifespan of such constants is usually large — often the whole module or even the whole codebase — so when we read the code, we usually don’t see the constant definition, but we can still assume that the value never changes. In addition, using such a constant in a function doesn’t make the function impure.
However, there’s an important difference between a variable defined with the const keyword and a true constant in JavaScript. The first only tells the compiler and the reader that the variable won’t be reassigned. The second describes the nature of the value as something global and static that never changes at runtime.
Both conventions reduce the cognitive load a bit and make the code easier to understand.
Unfortunately, JavaScript has no true constants, and mutation is still possible even when we define a variable using the const keyword.
Info We talk about mutation in the next chapter, Avoid mutation.
Code without reassignments is often easier to reason about because variables don’t change their values in the middle of their lifespan.
However, reassignments aren’t pure evil, and exterminating them at all costs won’t make the code better. I treat each reassignment as a sign: if I see a reassignment, I ask myself whether rewriting the code without it would improve readability. There’s no right or wrong answer, but if we do use reassignments, it’s better to isolate them in small functions, making it clear what the current value of a variable is.
Start thinking about:
Using different variables with meaningful names instead of reusing the same variable for different purposes.
Separating the data from the algorithm to make code more readable and maintainable.
Building a complete complex object in a single place instead of building it field by field.
Declaring variables as close as possible to where they are used to reduce their lifespan and to make it easier to understand which value a variable has at any particular moment.
Extracting a piece of code into a small function to avoid a temporary variable, and using a function return value instead.