Compile-time Immutability in TypeScript

Spread the love
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  

TypeScript allows us to decorate specification-compliant ECMAScript with type information that we can analyze and output as plain JavaScript using a dedicated compiler. In large-scale projects, this sort of static analysis can catch potential bugs ahead of resorting to lengthy debugging sessions, let alone deploying to production. However, reference types in TypeScript are still mutable, which can lead to unintended side effects in our software.
In this article, we’ll look at possible constructs where prohibiting references from being mutated can be beneficial.
Primitives vs Reference Types
JavaScript defines two overarching groups of data types:
Primitives: low-level values that are immutable (e.g. strings, numbers, booleans etc.)
References: collections of properties, representing identifiable heap memory, that are mutable (e.g. objects, arrays, Map etc.)
Say we declare a constant, to which we assign a string:
const message = ‘hello’;

Given that strings are primitives and are thus immutable, we’re unable to directly modify this value. It can only be used to produce new values:
console.log(message.replace(‘h’, ‘sm’)); // ‘smello’
console.log(message); // ‘hello’

Despite invoking replace() upon message, we aren’t modifying its memory. We’re merely creating a new string, leaving the original contents of message intact.
Mutating the indices of message is a no-op by default, but will throw a TypeError in strict mode:
‘use strict’;

const message = ‘hello’;
message[0] = ‘j’; // TypeError: 0 is read-only

Note that if the declaration of message were to use the let keyword, we would be able to replace the value to which it resolves:
let message = ‘hello’;
message = ‘goodbye’;

It’s important to highlight that this is not mutation. Instead, we’re replacing one immutable value with another.
Mutable References
Let’s contrast the behavior of primitives with references. Let’s declare an object with a couple of properties:
const me = {
name: ‘James’,
age: 29,
};

Given that JavaScript objects are mutable, we can change its existing properties and add new ones:
me.name = ‘Rob’;
me.isTall = true;

console.log(me); // Object { name: “Rob”, age: 29, isTall: true };

Unlike primitives, objects can be directly mutated without being replaced by a new reference. We can prove this by sharing a single object across two declarations:
const me = {
name: ‘James’,
age: 29,
};

const rob = me;

rob.name = ‘Rob’;

console.log(me); // { name: ‘Rob’, age: 29 }

JavaScript arrays, which inherit from Object.prototype, are also mutable:
const names = [‘James’, ‘Sarah’, ‘Rob’];

names[2] = ‘Layla’;

console.log(names); // Array(3) [ ‘James’, ‘Sarah’, ‘Layla’ ]

What’s the Issue with Mutable References?
Consider we have a mutable array of the first five Fibonacci numbers:
const fibonacci = [1, 2, 3, 5, 8];

log2(fibonacci); // replaces each item, n, with Math.log2(n);
appendFibonacci(fibonacci, 5, 5); // appends the next five Fibonacci numbers to the input array

This code may seem innocuous on the surface, but since log2 mutates the array it receives, our fibonacci array will no longer exclusively represent Fibonacci numbers as the name would otherwise suggest. Instead, fibonacci would become [0, 1, 1.584962500721156, 2.321928094887362, 3, 13, 21, 34, 55, 89]. One could therefore argue that the names of these declarations are semantically inaccurate, making the flow of the program harder to follow.
Pseudo-immutable Objects in JavaScript
Although JavaScript objects are mutable, we can take advantage of particular constructs to deep clone references, namely spread syntax:
const me = {
name: ‘James’,
age: 29,
address: {
house: ‘123’,
street: ‘Fake Street’,
town: ‘Fakesville’,
country: ‘United States’,
zip: 12345,
},
};

const rob = {
…me,
name: ‘Rob’,
address: {
…me.address,
house: ‘125’,
},
};

console.log(me.name); // ‘James’
console.log(rob.name); // ‘Rob’
console.log(me === rob); // false

The spread syntax is also compatible with arrays:
const names = [‘James’, ‘Sarah’, ‘Rob’];
const newNames = […names.slice(0, 2), ‘Layla’];

console.log(names); // Array(3) [ ‘James’, ‘Sarah’, ‘Rob’ ]
console.log(newNames); // Array(3) [ ‘James’, ‘Sarah’, ‘Layla’ ]
console.log(names === newNames); // false

Thinking immutably when dealing with reference types can make the behavior of our code clearer. Revisiting the prior mutable Fibonacci example, we could avoid such mutation by copying fibonacci into a new array:
const fibonacci = [1, 2, 3, 5, 8];
const log2Fibonacci = […fibonacci];

log2(log2Fibonacci);
appendFibonacci(fibonacci, 5, 5);

Rather than placing the burden of creating copies on the consumer, it would be preferable for log2 and appendFibonacci to treat their inputs as read-only, creating new outputs based upon them:
const PHI = 1.618033988749895;

const log2 = (arr: number[]) => arr.map(n => Math.log2(2));
const fib = (n: number) => (PHI ** n – (-PHI) ** -n) / Math.sqrt(5);

const createFibSequence = (start = 0, length = 5) =>
new Array(length).fill(0).map((_, i) => fib(start + i + 2));

const fibonacci = [1, 2, 3, 5, 8];
const log2Fibonacci = log2(fibonacci);
const extendedFibSequence = […fibonacci, …createFibSequence(5, 5)];

By writing our functions to return new references in favor of mutating their inputs, the array identified by the fibonacci declaration remains unchanged, and its name remains a valid source of context. Ultimately, this code is more deterministic.
The post Compile-time Immutability in TypeScript appeared first on SitePoint.

X ITM Cloud News

Emily

Next Post

Kotlin Cheat Sheet and Quick Reference [FREE]

Sun Nov 24 , 2019
Spread the love          Download a handy 2-page PDF Kotlin Cheat Sheet and Quick Reference! X ITM Cloud News
X- ITM

Cloud Computing – Consultancy – Development – Hosting – APIs – Legacy Systems

X-ITM Technology helps our customers across the entire enterprise technology stack with differentiated industry solutions. We modernize IT, optimize data architectures, and make everything secure, scalable and orchestrated across public, private and hybrid clouds.

This image has an empty alt attribute; its file name is x-itmdc.jpg

The enterprise technology stack includes ITO; Cloud and Security Services; Applications and Industry IP; Data, Analytics and Engineering Services; and Advisory.

Watch an animation of  X-ITM‘s Enterprise Technology Stack

We combine years of experience running mission-critical systems with the latest digital innovations to deliver better business outcomes and new levels of performance, competitiveness and experiences for our customers and their stakeholders.

X-ITM invests in three key drivers of growth: People, Customers and Operational Execution.

The company’s global scale, talent and innovation platforms serve 6,000 private and public-sector clients in 70 countries.

X-ITM’s extensive partner network helps drive collaboration and leverage technology independence. The company has established more than 200 industry-leading global Partner Network relationships, including 15 strategic partners: Amazon Web Services, AT&T, Dell Technologies, Google Cloud, HCL, HP, HPE, IBM, Micro Focus, Microsoft, Oracle, PwC, SAP, ServiceNow and VMware

.

X ITM