iTranslated by AI
TypeScript Coding Techniques #2: Loops
About this Article
This is the second installment in a series introducing TypeScript coding techniques.
You can find the first article here -> TypeScript Coding Techniques #1 (Conditional Branching Edition)
The intended audience is those who have used TypeScript to some extent and want to further improve their code quality. It is probably not for beginners.
The second part focuses on techniques for writing loop processes.
Comparison of Loops
We will examine the use cases for the following six types of loop processes while comparing their characteristics.
Since this has become a long article approaching 10,000 characters, I recommend reading only specific chapters as needed.
forstatement-
for...ofstatement (andfor...instatement andfor await...ofstatement) -
whilestatement (anddo...whilestatement) forEach()function- Functions that calculate values from arrays (and functions that manipulate arrays)
- Recursive function calls
for Statement
The for statement is the most basic loop process.
Compared to while, it is better at manipulating array indices and is suitable for processes that use multiple elements within an array or for processing multi-dimensional arrays.
Sample Code
Sample Code
// Example: Processing multi-dimensional arrays (Board game win determination)
type Mark = "o" | "x";
type Cell = "-" | Mark;
type FixedLengthArray<T, L extends unknown[]['length'], A extends T[] = []> = A['length'] extends L
? A
: FixedLengthArray<T, L, [T, ...A]>;
const size = 3;
type Row = FixedLengthArray<Cell, typeof size>;
type Board = FixedLengthArray<Row, typeof size>;
// const board: Board = [
// ["x", "o", "x"],
// ["-", "o", "-"],
// ["o", "x", "-"],
// ];
const wins = (board: Board, mark: Mark): boolean => {
let topLeftDiagonal = false;
let topRightDiagonal = false;
const eq = (cell?: Cell): boolean => cell === mark;
for (let i = 0; i < size; i++) {
// Win if a row is completed
if (board[i]?.every(eq) === true) return true;
// Win if a column is completed
if (board.every((row) => eq(row[i]))) return true;
// Win if a diagonal is completed
const j = size - 1 - i;
topLeftDiagonal &&= eq(board[i]?.[i]);
topRightDiagonal &&= eq(board[j]?.[j]);
}
return topLeftDiagonal || topRightDiagonal;
};
// Can be replaced with a range() generator function and for...of
for (const i of range(size)) {
// ...
}
Characteristics
It can be used generically and allows for complex processing.
Compared to the while statement, a distinctive feature is the ability to specify "initialization" and "iteration" expressions.
Other features are the same as the while statement.
Strengths and Best Use Cases
Compared to the for...of statement, it allows for direct manipulation of array indices, making it excellent for processes that use multiple elements within an array or multi-dimensional array processing.
It is suitable for "finite loop processes that change values at constant intervals," such as handling array indices.
Weaknesses and Constraints
It allows for many things compared to statements like for...of, but conversely, the code becomes more complex. If you are just processing array elements in order, it is better to use for...of.
If you pre-define a range() function like in Python, you can replace its role with for...of. This makes it safer because the index value used for iteration becomes a const instead of a let.
In my personal opinion, I try to use the for statement only for finite processes that change values at fixed intervals. For complex loop processes with many break or continue statements, or infinite loops, I believe it's clearer to write them with a while statement and extract them into a function.
Also, for processes that manipulate an array to calculate a new array or calculate a value from an array, it is better to use functions that calculate values from arrays.
for...of Statement
The for...of statement is a simple loop statement.
It allows writing loop processes for iterators such as arrays relatively concisely and safely.
Sample Code
Sample Code
// Example: Process to output array elements to standard output in order
const list = ["zero", "one", "two", "three"] as const;
// In case of a for statement
for (let i = 0; i < list.length; i += 1) {
const item = list[i];
console.log(list[i]);
}
// In case of a for...of statement
for (const item of list) {
console.log(item);
}
Characteristics
Compared to for, its characteristics are:
- It always processes elements of the iterator one by one.
- It doesn't make you conscious of the index.
Strengths and Best Use Cases
It is excellent at processing array elements one by one.
Compared to the for statement, it reduces the risk of accidentally creating infinite loops or accessing incorrect indices.
Weaknesses and Constraints
It is not suitable for complex processing that requires direct manipulation of indices, such as processing multiple elements within an array or using multi-dimensional arrays.
Also, for processes that manipulate an array to calculate a new array or calculate a value from an array, it is better to use functions that calculate values from arrays.
for...in Statement
The for...in statement iterates over the enumerable keys of an object.
However, object data structures (hash tables) are not suited for loop processing. I recommend using Map or arrays, which are data structures more suitable for iteration.
for await...of Statement
The for await...of statement is used when processing asynchronous generator functions and similar structures. Asynchronous processing will be introduced in detail in a later installment, so the explanation is omitted here.
while Statement
The while statement is a basic loop process alongside the for statement.
It is mainly used for processes that involve infinite loops.
Sample Code
Sample Code
// Example: Process to accept input interactively (board game input processing)
type Player = {
readonly name: string;
readonly mark: "o" | "x";
};
const player1: Player = {
name: "player 1",
mark: "o",
};
const player2: Player = {
name: "player 2",
mark: "x",
};
let player = player1;
const nextTurn = () => {
player = player === player1 ? player2 : player1;
};
// Accept player inputs alternately in an infinite loop
while (true) {
// Request and accept input asynchronously
boardIsFull = await requireInput(board, player);
// End if either one wins
if (wins(board, player.mark)) {
showWinner(player);
break;
}
// End in a draw if the board is full
if (!boardIsFull) {
showDraw();
break;
}
// Change turns
nextTurn();
}
// In case of a for statement
for (
let player = player1;
true;
player = player === player1 ? player2 : player1
) {
// ...
}
Characteristics
It is a statement that can be used generically.
Except for the fact that you cannot declare variables used for iteration within its scope, its capabilities are exactly the same as the for statement.
Complex loops, such as infinite loops, are often written using the while statement.
Strengths and Best Use Cases
It is excellent for infinite loops and complex processing.
It is suitable for event-driven asynchronous processing where infinite loops are used.
Weaknesses and Constraints
If you are just handling iterators like arrays, there are more suitable methods.
do...while Statement
The do...while statement differs from for and while in that the loop termination condition is evaluated at the end of the process.
I do not use it (I replace it with an infinite loop using while and a break).
forEach() function
The forEach() function is a method for looping through arrays.
It allows you to describe a loop process immediately following array operations in a method chain.
Sample Code
Sample Code
// Example: Process to output array elements to standard output in reverse order
const list = ["zero", "one", "two", "three"] as const;
// In case of a for statement
for (let i = list.length - 1; i >= 0; i -= 1) {
const item = list[i];
console.log(item);
}
// In case of the forEach() function
list.toReversed().forEach((item) => {
console.log(item);
});
Characteristics
Compared to the for...of statement, its characteristics are:
- It can be written following a method chain such as
map(),filter(), ortoSorted(). - The index can be obtained as the second argument.
- Since it is a function, you cannot use
break,continue, or an earlyreturn(to exit the loop). - Since it is a function, asynchronous processes cannot be processed sequentially with
await. - It does not perform processing on empty items.
Strengths and Best Use Cases
The biggest advantage is the method chain. Since the processing content becomes clear through the function names, the source code becomes easier to read.
Weaknesses and Constraints
In functions like forEach(), it is important to note that sequential execution of asynchronous processes using await is not possible.
For sequential execution of asynchronous processes, the for...of statement or a for await...of statement using an asynchronous generator is recommended.
Also, since there are performance concerns with the standard method chain, I recommend using the external packages introduced in the next chapter.
Functions that Calculate Values from Arrays
Using built-in functions such as map() and filter(), you can calculate new arrays or values from an array.
These functions can be linked in a method chain and written concisely.
Strengths and Best Use Cases
Since a function call is an expression, it is better at calculating values than for or for...of statements.
When using statements like for or for...of, you cannot make the values "constants" or "read-only".
As mentioned at the beginning of the previous article, data used in programming should, in principle, be "constants" or "read-only."
Mutable values that are no longer under your direct control make it impossible to predict when or where they might be changed, which significantly hinders the clarity of the logic.
Also, since you are using existing functions, you get benefits such as:
- You don't have to implement logic like sorting yourself.
- The source code becomes easier to read because the processing content is clear from the function names.
- Implementation and review costs are reduced, resulting in a lower risk of bugs.
Weaknesses and Constraints
For better or worse, they lack the versatility of other loop processes and can only be used for specific operations.
Furthermore, built-in method chains repeat loop processing multiple times, so they are inferior to for or for...of in terms of performance.
Using an npm package with lazy evaluation features that can optimize loop processing allows you to achieve both readability and performance.
While the famous Lodash is fine, I highly recommend Remeda (as of the time of writing).
Sample Code
Sample Code
// Example: Calculating an array from an array
import * as R from "remeda";
const result = R.pipe(
["zero", "ZERO", "one", "ONE", "two", "TWO", "three", "THREE"] as const,
// Convert to uppercase
R.map((value) => value.toUpperCase()),
// Remove duplicates
R.uniq,
// Filter for those containing "E"
R.filter((value) => value.includes("E")),
);
console.log(result); // ['ZERO', 'ONE', 'THREE']
// I thought about writing a bad example using a for statement, but I'll give up because I don't feel like I can write it.
Functions that Manipulate Arrays
Built-in functions like reverse() and sort() overwrite the original array instead of calculating a new one.
Since the array is no longer "read-only," it is generally better not to use them.
Use them only when unavoidable, such as in situations where high performance is required or state management is indispensable.
Recursive Function Calls
Recursive function calls can also be used as a form of loop processing.
They are characterized by maintaining the call stack, making them excellent for tasks like depth-first search.
In pure functional programming languages, all iterations are written using recursive calls.
Sample Code
Sample Code
// Example: Loop processing that branches in a tree-like manner (Generating a Fibonacci sequence)
const fibSeq = [1, 1];
const fib = function (n: number): number {
// Return the value if it has already been calculated
const memo = fibSeq[n];
if (memo !== undefined) return memo;
// Recursive processing that branches in a tree-like manner
const value = fib(n - 1) + fib(n - 2);
// Save the calculation result to a memo
fibSeq[n] = value;
return value;
};
fibs(n);
// There are more efficient ways if you only need to find a Fibonacci "number"
Characteristics
As the name suggests, it is excellent at recursive processing.
By utilizing the call stack, tasks like depth-first search can be written efficiently.
Strengths and Best Use Cases
You can intuitively write recursive processes like depth-first search.
Weaknesses and Constraints
Performance is poor. It is slower than procedural processing and consumes more memory.
It is mathematically proven that recursive calls and loop procedures are completely equivalent. In other words, any process written with a recursive call can also be written using a loop procedure.
Sample code rewritten as a loop procedure
const fibSeq = [1, 1]
for (let i = 2; i <= n; i += 1) {
fibSeq[i] = fibSeq[i-1] + fibSeq[i-2];
}
Also, since the call stack accumulates in memory, it is not suitable for a large number of repetitions.
Too many iterations will cause the famous Stack Overflow.
Discussions regarding proper tail call optimization, standardized in ES2015 (ES6), are still ongoing, and only a handful of JavaScript execution environments support it.
Summary
Since it's become long, I'll summarize it in a table.
| Classification | Best Use Cases | Versatility (Complexity) |
|---|---|---|
for statement |
Processes handling array indices (can be replaced with for...of and range()) |
High |
for...of statement |
Processes handling each array element | Medium (Finite) |
for...in statement |
Processes handling each key of an object | Medium (Finite) |
for await...of statement |
Processes handling asynchronous generators | Medium (Finite) |
while statement |
Infinite loops, complex loops | High |
do...while statement |
Do not use | High |
forEach() function |
Processing after array element modification (external package recommended) | Slightly Low (No break) |
| Functions that calculate values from arrays | Processes calculating arrays or values from an array (external package recommended) | Low |
| Functions that manipulate arrays | Processes for array state management | Slightly Low (No break) |
| Recursive function calls | Processes that are clearer when written recursively (poor performance) | High |
By clarifying the purpose of the processing and choosing the "one with lower versatility and more limited use," you can resolve code complexity and make it easier to read.
Next Time
I will introduce techniques for organizing code using functions.
Discussion