0%
Reading Settings
Font Size
18px
Line Height
1.5
Letter Spacing
0.01em
Font Family
Table of contents
    blog cover

    Make Our Utils Functions Immutable

    Software Engineer
    Software Engineer
    published 2025-11-22 18:12:27 +0700 · 3 mins read
    I chose JavaScript for this blog because it has both mutable and immutable objects and methods,  while language like Haskell enforces immutability everywhere

    1. Mutable vs Immutable Objects and Methods

    When we say an object is immutable, we mean it cannot be changed after it’s created. Strings in JavaScript are an example:
    // language: javascript
    const s = "hello"
    console.log(s[0]) // "h"
    s[0] = "H"
    console.log(s)    // "hello"
    No matter what you do, the original string stays the same. Arrays, on the other hand, are mutable:
    // language: javascript
    const nums = [1, 2, 3]
    nums[0] = 4
    console.log(nums) // [4, 2, 3]

    Many array methods also mutate the array. For example:
    // language: javascript
    const numbers = [3, 1, 2]
    numbers.sort()
    console.log(numbers) // [1, 2, 3] - original array changed!
    Besides, modern JavaScript provides immutable alternatives, like toSorted():
    // language: javascript
    const numbers = [3, 1, 2]
    const sorted = numbers.toSorted()
    console.log(sorted)  // [1, 2, 3]
    console.log(numbers) // [3, 1, 2] - original stays the same

    2. Take a look at this example

    // language: javascript
    const countEven = (arr) => {
      let count = 0
      while (arr.length > 0) {
        if (arr.pop() % 2 === 0) count += 1
      }
      return count
    }
    
    const a = [1, 2, 3, 4]
    console.log(countEven(a))
    console.log(a)

    Wait 20 seconds and guess the result :D
    20 seconds
    10 seconds
    5 seconds
    end!

    // language: javascript
    const a = [1, 2, 3, 4]
    console.log(countEven(a)) // 2
    console.log(a) // []
    Why? pop() is mutable, it removes items from the original array. The function works, but it destroys the input, which is usually not what you want.

    3. Make it immutable

    We can rewrite the same function without touching the input:
    // language: javascript
    const countEven = (arr) => {
      return arr.filter(n => n % 2 === 0).length
    }
    
    const a = [1, 2, 3, 4]
    console.log(countEven(a)) // 2
    console.log(a)            // [1, 2, 3, 4]
    filter() returns a new array, leaving the original intact so that the function becomes predictable and safe.

    4. Quick Tips for Writing Immutable Utils

    Avoid mutating input arrays or objects

    Methods like pop(), shift(), splice(), sort(), and reverse() change the original. Use immutable alternatives:
    | Mutable               | Immutable alternative             |
    |-----------------------|-----------------------------------|
    | arr.pop()             | arr.slice(0, -1)                  |
    | arr.shift()           | arr.slice(1)                      |
    | arr.splice(start, n)  | arr.toSpliced(start, n)           |
    | arr.sort()            | arr.toSorted()                    |
    | arr.reverse()         | arr.toReversed()                  |

    Prefer map, filter, and reduce for transformations

    Instead of manually looping and modifying:
    // language: javascript
    // Mutable (avoid)
    for (let i = 0; i < arr.length; i++) {
      arr[i] *= 2
    }
    
    // Immutable
    const doubled = arr.map(x => x * 2)

    Use readonly in TypeScript

    This signals clearly that the function does not modify its input:
    // language: javascript
    function normalizeUsers(users: readonly User[]) {
      return users.map(u => ({ ...u, name: u.name.trim() }))
    }
    Trying to mutate users will result in a compile-time error.

    Return new objects/arrays instead of mutating

    Always assume your utility might be reused elsewhere. Avoid side effects:
    // language: javascript
    // Bad: modifies input
    function addUser(users, user) {
      users.push(user)
      return users
    }
    
    // Good: returns new array
    function addUser(users, user) {
      return [...users, user]
    }

    Deep immutability when needed

    If your function handles nested objects and you want safety across all levels:
    // language: javascript
    const newUsers = users.map(u => ({
      ...u,
      address: { ...u.address }
    }))
    Libraries like Immer or Immutable can make this easier without deep manual copying.

    Use linting rules to enforce immutability

    Tools like ESLint can prevent accidental mutations:
    // language: javascript
    "no-param-reassign": ["error", { "props": true }],
    "functional/immutable-data": "error"
    This helps large teams maintain predictable utilities and avoid subtle bugs.

    5. Summary

    In short, immutable utilities are safer because they don’t unexpectedly change their inputs. While mutable methods can be fine for local variables or performance-critical code, they should be avoided in shared utilities. By keeping our utility functions immutable, our code becomes predictable, easier to reason about, which is especially important in larger projects.

    Related blogs