JAVASCRIPT / NUMBERS, MATH, AND BIGINT
Min, max, clamping, and range helpers
Clamp values into a range, fold min and max over arrays without blowing the argument limit, and remap numbers between ranges with normalize and lerp.
What you will learn
- Write clamp(x, lo, hi) as Math.min(Math.max(x, lo), hi) and know why the order matters
- Explain why Math.max() returns -Infinity and use ±Infinity as reduce seeds
- Get min and max from a large array with reduce instead of spread
- Remap a value between ranges by normalizing to 0..1, clamping, then interpolating
Understanding Min, max, clamping, and range helpers
Math.min and Math.max are variadic folds over numbers, not array functions. They coerce every argument with ToNumber first, so Math.max(true, null) is 1 and Math.max("7", "12") is 12 rather than the string comparison you might expect. Called with no arguments at all, Math.max returns -Infinity and Math.min returns Infinity, because those are the identity values for the two operations: nothing can beat -Infinity in a max, so folding an empty list has to land there.
That identity fact is directly useful. When you fold an array yourself, -Infinity is the correct seed for a max and Infinity for a min, since Math.max(-Infinity, x) is x for every non-NaN x. Clamping is the same two folds composed: Math.min(Math.max(x, lo), hi) pushes x up to the floor, then pulls it down to the ceiling. JavaScript has no Math.clamp, so this one-liner is the idiom, and the order only becomes visible when lo > hi, where the outer call wins and you silently get hi back instead of an error.
Everything else about ranges factors into two operations. Normalizing, (x - lo) / (hi - lo), turns a value into a 0-to-1 position inside its range; interpolating, a + (b - a) * t, turns a 0-to-1 position back into a real value. Remapping is normalize followed by interpolate, and whether you clamp the intermediate t decides if out-of-range inputs saturate at the edges or extrapolate past them. For a membership test, x >= lo && x <= hi is enough, and it excludes NaN for free, since every comparison involving NaN is false.
NaN is the one value that breaks all of this quietly. Math.max returns NaN if any argument is NaN, so clamp(NaN, 0, 10) is NaN, not 0 — clamping cannot repair a bad number, it can only bound a good one.
const clamp = (x, lo, hi) => Math.min(Math.max(x, lo), hi);
console.log(clamp(150, 0, 100));
console.log(clamp(-20, 0, 100));
console.log(clamp(42, 0, 100));
const scores = [88, 61, 97, 74];
console.log(Math.max(...scores));
console.log(Math.max(scores));
console.log(Math.max());
console.log(Math.min());Min and max are folds with identity values -Infinity and Infinity, and clamping is just those two folds composed: raise to the floor, then lower to the ceiling.
Worked examples
Coercion, NaN, and signed zero
Shows what Math.max and Math.min actually do to their arguments before comparing them.
const clamp = (x, lo, hi) => Math.min(Math.max(x, lo), hi);
console.log(clamp(NaN, 0, 10));
console.log(Math.max(3, NaN, 9));
console.log(Math.max("7", "12"));
console.log(Math.max(true, null));
console.log(Object.is(Math.min(0, -0), -0));Example explained
Line 1clamp(NaN, 0, 10) is NaN because Math.max(NaN, 0) is already NaN and the outer Math.min just passes it along.
Line 2Math.max("7", "12") is 12: arguments go through numeric coercion, unlike "12" > "7", which compares character by character and is false.
Line 3Math.max(true, null) compares 1 and 0, since ToNumber turns true into 1 and null into 0.
Line 4Math.min treats -0 as smaller than +0 and returns it, which only Object.is or 1 / result can reveal.
Min and max over a large array
Folds a 100,000-element array with the identity seeds instead of spreading it into a call.
const data = Array.from({ length: 100000 }, (_, i) => ((i * 7919) % 1000) - 500);
// Math.max(...data) risks a RangeError once the array grows this big.
let lo = Infinity;
let hi = -Infinity;
for (const n of data) {
if (n < lo) lo = n;
if (n > hi) hi = n;
}
console.log(lo, hi);
const hiViaReduce = data.reduce((m, n) => Math.max(m, n), -Infinity);
console.log(hiViaReduce, data.length);Example explained
Line 1The seeds Infinity and -Infinity are exactly what Math.min() and Math.max() return with no arguments, so they can never win against real data.
Line 2The loop and the reduce agree on 499 because each element is compared one at a time, never passed as a separate function argument.
Line 3Spreading with ...data would try to push 100,000 arguments onto the call stack, which engines reject with a RangeError past a few tens of thousands.
Line 4The values run from -500 to 499 because (i * 7919) % 1000 cycles through every residue 0 to 999 before the shift.
Normalize, interpolate, remap
Builds a range remapper out of three small helpers and shows how clamping makes it saturate instead of extrapolate.
const clamp = (x, lo, hi) => Math.min(Math.max(x, lo), hi);
const lerp = (a, b, t) => a + (b - a) * t;
const norm = (x, lo, hi) => (x - lo) / (hi - lo);
const remap = (x, inLo, inHi, outLo, outHi) =>
lerp(outLo, outHi, clamp(norm(x, inLo, inHi), 0, 1));
console.log(norm(75, 50, 100));
console.log(lerp(0, 255, 0.25));
console.log(remap(20, 0, 40, -1, 1));
console.log(remap(90, 0, 40, -1, 1));Example explained
Line 1norm(75, 50, 100) is 0.5 because 75 sits halfway between the bounds; the result is a position, not a value.
Line 2lerp(0, 255, 0.25) walks a quarter of the distance from 0 to 255, giving 63.75.
Line 3remap(20, 0, 40, -1, 1) normalizes to 0.5 and interpolates it into -1..1, landing on 0.
Line 4remap(90, ...) normalizes to 2.25, but clamp cuts it to 1, so the result saturates at the top of the output range instead of overshooting to 3.5.
Important notes
There is no Math.clamp in JavaScript; CSS has clamp() but the language does not, so you write your own. Also remember Math.min(Math.max(x, lo), hi) returns hi when lo > hi, while flipping the two calls returns lo, so check the range rather than relying on either.
The lerp form a + (b - a) * t returns exactly a at t === 0 but is not guaranteed to land exactly on b at t === 1; use (1 - t) * a + t * b when hitting both endpoints exactly matters.
Common mistakes
Passing an array directly: Math.max([3, 9, 4]) returns NaN because the array stringifies to "3,9,4" and then coerces to NaN. Nothing throws, and the NaN quietly infects every later calculation. Worse, Math.max([7]) returns 7, so a one-element test case passes.
Spreading a big array: Math.max(...values) works fine on 100 rows and throws a RangeError on 100,000, so the bug only appears once real data volume arrives. Fold with reduce instead.
Seeding a hand-rolled max loop with 0 instead of -Infinity. On all-negative data such as [-8, -3, -12] you get 0 back, a value that was never in the array, with no error to point at it.
Try it yourself
Change, predict, then run
Write clampIndex(arr, i) that returns a usable index for any integer i, so clampIndex(['a','b','c'], 9) is 2 and clampIndex(['a','b','c'], -4) is 0. Then call it with an empty array, see what arr.length - 1 does to your clamp, and handle that case explicitly.
Open the JavaScript workspaceCheck your understanding
You compute a maximum with temps.reduce((m, t) => Math.max(m, t), seed). Why is -Infinity a better seed than 0?
- -Infinity is the identity for max, so it can never beat a real reading, while 0 wins against any all-negative data and reports a temperature that was never measured
- -Infinity makes the fold faster, because comparing against an infinity lets the engine skip numeric coercion
- 0 is fine as a seed; Math.max internally substitutes -Infinity whenever the accumulator would be wrong
- -Infinity forces the accumulator to stay a float, which prevents integer overflow over long arrays
Show answer
Math.max(-Infinity, x) is x for every non-NaN x, which is precisely what an identity value means and the same reason Math.max() with no arguments is -Infinity. Seeding with 0 installs a hidden floor: [-8, -3, -12] folds to 0 with no error raised. Option three is the tempting one, but Math.max never inspects or rewrites your seed; the seed is just another argument handed to it on the first iteration.