JavaScript is beautiful ❤️🔥 but there are some caveats you must know about—one of them is mutation.
Let’s take a deeper dive into this effect and understand once and for all what it’s all about and how you can overcome this effect in an elegant way.
To illustrate the problems, most of the explanations will be covered with clear examples that you can easily test on your side, even through your Browser’s console.
So enough chit-chat, let’s get straight to the point.
What’s precisely mutation?
Mutation
is an alteration of the existing value(s) in a data structure such as Object
, Array
, Function
, Map
, Set
, and so forth.
It happens every time you add
, remove
, or modify
the initial data state.
Examples of Mutation
Here are some examples:
/**--------------------- ARRAY ---------------------*/
const species = ['octopus', 'squid', 'shark',];
console.log('initial state', species);
// outputs: 'octopus', 'squid', 'shark'
// 'push' method mutates the array
// by adding a new item to its set
species.push('seahorse');
console.log('mutated state', species);
// outputs: 'octopus', 'squid', 'shark', 'seahorse'
/**--------------------- OBJECT ---------------------*/
const person = { name: 'John Doe', };
console.log('initial state', person.name);
// outputs: 'John Doe'
// overwrites the initial property value
person.name = 'Janie Doe';
console.log('mutated state', person.name);
// outputs: 'Janie Doe'
Are there immutable data types?
Yes! All Primitives (primitive values and primitive data types) are immutable by nature, meaning they cannot be altered.
Example of Immutable Data Types
const person = 'John Doe';
console.log('initial', person);
// outputs: 'John Doe'
// try to mutate the data structure
// by using the 'upper case' method
person.toUpperCase();
console.log('mutation attempt', person);
// outputs: 'John Doe'
As shown above, calling .toUpperCase()
does not mutate the original string because strings are immutable.
The undesired mutation side-effects
As you’ve probably seen, it’s easy to mutate a data structure, and this can introduce undesired side effects, such as:
- Surprises: Your code becomes unpredictable and no longer deterministic.
- Loss of data reliability: Your data may no longer be trustworthy as you cannot ensure its purity.
- Increased maintainability cost: You might spend more time refactoring and maintaining your code due to unexpected behavior.
- Bugs: All of the above can lead to bugs in your application.
Real-World Example of Mutation Side-Effects
/**
* Encode credit card number, except last 4 digits
*/
const maskSensitivityInfos = (card) => {
card.number = card.number.replace(/.(?=.{4})/g, '#');
return card;
}
/**
* Simulate the display of payment info.
* This is usually done to confirm the selected payment method.
*/
const displaySelectedPayment = (card) => {
console.log(`Paying with ${card.type} ${card.number}`);
}
/**
* Simulate the payment processing.
* This is the part where we make the payment transactions
* so the REAL (unmodified) DATA is required in this step.
*/
const processPayment = (card) => {
console.log('Card details', card);
};
const creditCard = { type: 'MasterCard', number: '5431111111111111', cvv: '595' };
// we mask sensivity infos to be displayed in the screen
// this is a standard security approach that you might have seen before
const maskedCreditCard = maskSensitivityInfos(creditCard);
displaySelectedPayment(maskedCreditCard);
// output: 'Paying with MasterCard ############1111'
processPayment(creditCard);
// output: 'Card details: { type: 'MasterCard', number: '############1111', cvv: '595' }'
// as you could spot, the card number was mutated and it is no longer valid
// to process the transaction
The method maskSensitivityInfos()
not only mutated the credit card number, introducing a critical bug, but also broke the entire payment processing pipeline of the application; and I’m pretty sure that someone would be not reeeally happy about it.
Fixing mutation
This is a fairly easy task! All we need to do is to copy the object before making changes.
Using Spread Operator
const maskSensitiveInfo = (card) => {
const cc = { ...card }; // creates a shallow copy
cc.number = cc.number.replace(/.(?=.{4})/g, '#');
return cc;
};
By creating a copy of the object, the original data remains unchanged: j
const creditCard = { type: 'MasterCard', number: '5431111111111111', cvv: '595' };
const maskedCreditCard = maskSensitiveInfo(creditCard);
displaySelectedPayment(maskedCreditCard);
// output: 'Paying with MasterCard ############1111'
processPayment(creditCard);
// output: 'Card details: { type: 'MasterCard', number: '5431111111111111', cvv: '595' }'
// No side effects! Data remains intact.
Copying Data Structures
To copy various data structures, you can use the following techniques:
- Array:
const copy = [...originalArray]
- Object:
const copy = { ...originalObject }
- Date:
const copy = new Date(originalDate)
- Map:
const copy = new Map(originalMap)
- Set:
const copy = new Set(originalSet)
Shallow Copy
Shallow Copy
is a code utility that centralize all copy related operations into a single place and takes care of the different copy strategies required from the data structure.
This approach has some great benefits, like:
- Single source of truth.
- Low refactoring effort required.
- Reusability.
Just be aware that, as a shallow
operation, this only affects the first level of the structure, meaning that anything bellow the 1st level, will be unaffected.
This will generally satisfy almost all your needs, but in case you need to have a deep copy, you can easily update the code to include a recursive operation over its items 😉
Dependency
We will be using an improved version of the JavaScript typeof()
operator, which we covered here before, if you haven’t read it yet, click in the link bellow and check that out!
Making JavaScript “typeof()” work
Formulas
ES6
import typeOf from './type-of';
/**
* Shallow copy
* @description Create a copy of a collection with the same structure.
* @param value
*/
const shallowCopy = (value) => {
if (typeOf(value) === 'array') {
return [...value];
} else if (typeOf(value) === 'object') {
return { ...value };
} else if (typeOf(value) === 'date') {
return new Date(value);
} else if (typeOf(value) === 'map') {
return new Map(value);
} else if (typeOf(value) === 'set') {
return new Set(value);
}
return value;
};
export default shallowCopy;
Typescript
import typeOf from './type-of';
/**
* Shallow copy
* @description Create a copy of a collection with the same structure.
* @param value
*/
const shallowCopy = <T>(value: T): T => {
if (typeOf(value) === 'array') {
return [...(value as unknown[])] as T;
} else if (typeOf(value) === 'object') {
return { ...(value as Record<string, unknown>) } as T;
} else if (typeOf(value) === 'date') {
return new Date(value as unknown as Date) as T;
} else if (typeOf(value) === 'map') {
return new Map(value as unknown as Map<unknown, unknown>) as T;
} else if (typeOf(value) === 'set') {
return new Set(value as unknown as Set<unknown>) as T;
}
return value;
};
export default shallowCopy;
Discussion