Skip to content
JavaScript Basics: Transform Arrays Without Changing the Original

JavaScript Basics: Transform Arrays Without Changing the Original

October 14, 2026

A list often needs several small changes before it is ready to display: remove unavailable items, calculate a value for each remaining item, and put the results in order. JavaScript has array methods for each job. This tutorial builds a tiny stock summary, then handles numeric sorting and empty input explicitly.

Set up an isolated workspace

You need Linux, Bash, the mktemp, rm, and rmdir utilities, a text editor, and Node.js on your PATH. The examples were tested with Node.js v24.19.0 on Linux. No packages, downloads, network access, or environment variables are required. The .mjs extension enables ES modules, including imports from Node’s built-in assertion module.

Run this in Bash and keep the same terminal open through cleanup:

node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

The tested version prints:

v24.19.0

Use your editor to save each named file below as UTF-8 in this temporary directory. Each file runs independently; there is no shared setup code to copy between files.

Select, transform, and total

Use filter() to choose entries and map() to calculate their output values. Keeping those jobs separate makes the intermediate results easy to inspect. A filter callback answers “keep this?”, while a map callback returns the new value. MDN documents filtering and mapping.

Save as summarize-stock.mjs:

import assert from "node:assert/strict";

const stock = [
  { name: "Notebook", quantity: 3, priceCents: 250 },
  { name: "Pen", quantity: 0, priceCents: 120 },
  { name: "Folder", quantity: 2, priceCents: 175 },
];
const before = JSON.stringify(stock);
const available = stock.filter((item) => item.quantity > 0);
const lines = available.map((item) => ({
  name: item.name,
  valueCents: item.quantity * item.priceCents,
}));
const totalCents = lines.reduce((sum, item) => sum + item.valueCents, 0);

assert.equal(totalCents, 1100);
assert.equal(JSON.stringify(stock), before);
assert.equal(available[0], stock[0]);
assert.notEqual(lines[0], stock[0]);
console.log(JSON.stringify(lines));
console.log(`Total cents: ${totalCents}`);

Run:

node summarize-stock.mjs

Expected output:

[{"name":"Notebook","valueCents":750},{"name":"Folder","valueCents":350}]
Total cents: 1100

The parentheses around the map callback’s object literal make it an expression to return. Without them, braces would start a function body and require an explicit return.

reduce() carries an accumulated value through the array. Here sum starts at the supplied 0, and each callback returns the next sum. An explicit starting value also makes an empty list safe. See MDN’s reduce reference.

These callbacks do not change the input. However, a new array does not mean deeply copied objects. The filtered array still refers to the original item objects; changing available[0].quantity would change the original item. The map callback deliberately creates new objects containing only the fields this report needs.

Sort numbers without rearranging the input

A default array sort compares string representations, which is surprising for numbers: 10 can precede 2. Supply a comparator for numeric ordering. A negative result puts the first value before the second; zero treats them as equal; a positive result puts it after. toSorted() returns a new array rather than rearranging its input. See MDN’s toSorted reference.

Save as sort-scores.mjs:

import assert from "node:assert/strict";

const scores = [10, 2, 30, 2];
const ascending = scores.toSorted((a, b) => a - b);
const descending = scores.toSorted((a, b) => b - a);

assert.deepEqual(ascending, [2, 2, 10, 30]);
assert.deepEqual(descending, [30, 10, 2, 2]);
assert.deepEqual(scores, [10, 2, 30, 2]);
assert.deepEqual([].toSorted((a, b) => a - b), []);
console.log(`Ascending: ${ascending.join(", ")}`);
console.log(`Original: ${scores.join(", ")}`);

Run:

node sort-scores.mjs

Expected output:

Ascending: 2, 2, 10, 30
Original: 10, 2, 30, 2

This comparator assumes finite numbers. Validate data from outside your program before sorting; strings and NaN need an explicit policy. Avoid boolean comparators such as (a, b) => a > b, which do not supply the negative result required for the reverse ordering.

Give empty input a useful result

“No matching entries” is an ordinary outcome, not necessarily an error. Choose a result that callers can use without a special recovery path.

Save as summarize-empty.mjs:

import assert from "node:assert/strict";

function totalPositive(values) {
  return values
    .filter((value) => value > 0)
    .map((value) => value * 2)
    .reduce((sum, value) => sum + value, 0);
}

assert.equal(totalPositive([]), 0);
assert.equal(totalPositive([-3, 0]), 0);
assert.equal(totalPositive([-3, 0, 2, 5]), 14);
assert.deepEqual([].map((value) => value * 2), []);
assert.throws(() => [].reduce((sum, value) => sum + value), TypeError);
console.log(`Empty total: ${totalPositive([])}`);
console.log(`Mixed total: ${totalPositive([-3, 0, 2, 5])}`);

Run:

node summarize-empty.mjs

Expected output:

Empty total: 0
Mixed total: 14

The assertions run silently when correct. An unexpected result stops the script, so successful output also confirms the checks passed.

Clean up

Remove only the files created for this tutorial, then remove the empty directory:

rm -- summarize-stock.mjs sort-scores.mjs summarize-empty.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Reach for filter to select, map to transform, and reduce to combine. Keep the steps readable, preserve the input when needed, and decide what empty data means before it reaches your interface.

Acknowledgements

This post was created with assistance from AI.

Last updated on