Immutability with Const Assertions

Type widening and type narrowing are fundamental concepts in TypeScript, especially when working with variables. Through the const and let keywords, you can control the scope of mutability and immutability in your code. In this chapter, we are putting our acquired knowledge about type widening, type narrowing, and data types to the test while extending it by exploring const assertions.

Type Widening with let Variables

When a variable is declared using the let keyword and initialized with a value, TypeScript infers a collective type for the variable. This allows the variableโ€™s value to be reassigned later with other compatible values:

let user = 'Benny';
user = 'Sofia';

In this example, the type of user is widened to string, enabling it to hold any string value, not just the initial value.

Type Narrowing with const Variables

When a variable is declared using the const keyword, TypeScript infers a literal type instead of a collective type. This behavior indicates that the value cannot be reassigned:

const user = 'Benny';
user = 'Sofia'; // Throws error

This behavior applies to all primitive types, which include string, number, boolean, null and undefined. These primitive types are immutable and passed by value when used in assignments or function arguments. While they do not have methods themselves, JavaScript temporarily coerces them into their corresponding object wrappers, allowing access to methods and properties.

Reference Types: Objects, Arrays, and Functions

In contrast to primitive types, values of reference types (e.g., objects, arrays, and functions) are mutable. Variables of these types store references (memory addresses) to the actual data. The address itself cannot be modified but the properties that are referenced are modifiable:

const user = { name: 'Benny', age: 35 };
user.name = 'Sofia';

Preventing Mutability with const assertions

To enforce immutability for reference types, TypeScript 3.4 introduced const assertions. Using the as const syntax, you can narrow down the type of a reference to a literal type and mark all properties as readonly:

const user = {
  name: 'Benny',
  age: 35,
} as const;
 
user.name = 'Sofia'; // Throws error

This ensures that the user object and its properties are immutable within TypeScript, though it does not alter the runtime behavior in plain JavaScript.

Arrays and const assertions

Arrays are regular objects and therefore fall under the category of reference types. When TypeScript infers the type of an array, it defaults to a collective type:

const array = [1, 2, 3]; // TypeScript infers "number[]"
const constant = array[0]; // Type is "number" (collective)
array[0] = 4; // Allowed

By applying a const assertion, you can narrow the type of the array to a tuple with literal types, ensuring immutability:

const array = [1, 2, 3] as const;
array[0] = 4; // Throws error
const constant = array[0]; // Type is "1" (literal)

This guarantees that the array and its elements remain unchanged within TypeScript code.


Runtime Behavior with Object.freeze

While const assertions enforce immutability in TypeScript, they do not affect runtime behavior in JavaScript. Remember that these constraints are enforced at compile time only and may not persist if transpiling your TypeScript code to JavaScript. If you need runtime immutability, you can use the Object.freeze method:

'use strict';
const array = [1, 2, 3];
Object.freeze(array);
 
array[0] = 4; // Throws error

Using Object.freeze ensures that the object or array cannot be modified at runtime. In combination with the "use strict" pragma, JavaScript will throw an error when attempting to mutate frozen objects or arrays.

Overview

Primitive TypesComplex Types
Examples- Null- Objects {}
- Undefined- Arrays []
- Boolean- Functions
- Number- User-Defined Objects (e.g., new Car)
- BigInt
- String
- Symbol
MutabilityImmutableMutable
Passed ByValue / CopyReference

What You Have Learned

Type Widening with let Variables: You have learned that when a variable is declared using let, TypeScript infers a collective type, which allows the variable to hold different values over time. For example, a variable initialized with a string can later hold any string value, offering flexibility in your code.

Type Narrowing with const Variables: You now understand that when a variable is declared using const, TypeScript infers a literal type, which prevents reassignment. This means the value of a const variable is fixed, and attempting to change it will result in an error. This behavior ensures immutability for primitive types.

Reference Types: Objects, Arrays, and Functions: You have learned that reference types, like objects, arrays, and functions, are mutable in JavaScript. While the memory address of the reference cannot be changed, the properties of the reference (like the values in an array or object) can still be modified.

Preventing Mutability with const assertions: You now understand how to enforce immutability for reference types using const assertions with the as const syntax. This narrows the type of a reference to a literal type and marks all its properties as readonly, making the reference immutable within TypeScript.

Arrays and const assertions: You have discovered that arrays are treated as reference types, and by applying const assertions, you can convert an array into a tuple with literal types. This ensures that the array and its elements cannot be changed, providing immutability.

Runtime Behavior with Object.freeze: You now know that while const assertions enforce immutability at compile time in TypeScript, they do not affect runtime behavior in JavaScript. For runtime immutability, you can use Object.freeze to prevent modifications to objects and arrays during execution.


Quiz

  1. How does TypeScript treat variables declared with the let keyword when they are initialized with a specific value?
  • a) It narrows them to a literal type based on the exact value.
  • b) It infers the any type automatically.
  • c) It infers a collective type that can be reassigned later.
  • d) It prevents any reassignment of the variable.
  1. Which statement best describes how TypeScript treats arrays declared with const, if you do not use as const?
  • a) The array and its elements are fully immutable, preventing any changes.
  • b) The array automatically becomes a tuple type.
  • c) The arrayโ€™s reference canโ€™t be reassigned, but its elements can still be modified.
  • d) The array is assigned the type any[] by default.
  1. Which of the following is true about reference types like objects and arrays when using const without a const assertion?
  • a) You cannot reassign the variable reference, but object properties or array elements can still be modified.
  • b) The object or array becomes completely immutable, and its contents cannot be changed.
  • c) TypeScript automatically converts them into primitive types.
  • d) They throw a compile-time error if you try to modify properties.
  1. What does the as const syntax do in TypeScript?
  • a) It applies Object.freeze at runtime, preventing any modifications to the object.
  • b) It narrows the type of a reference value to a literal type, marking all properties as readonly at compile time.
  • c) It widens the type of a variable, allowing for future reassignment.
  • d) It enables the variable to switch between different types dynamically.
  1. How can you achieve runtime immutability for an array in JavaScript (after TypeScript compilation) while in strict mode?
  • a) By declaring the array with let and assigning it a literal type.
  • b) By using as const to enforce literal types on the array.
  • c) By calling Object.freeze(array) so that any mutation attempts throw an error.
  • d) By enabling readonly mode in the JavaScript engine.
  1. Why does TypeScript distinguish between primitive and reference types when inferring types for variables declared with const?
  • a) Primitive types are automatically mutable, whereas reference types are always immutable.
  • b) Reference types can have their internal properties changed, whereas primitive types cannot be altered once created.
  • c) Reference types require explicit const assertions to become literal types.
  • d) Reference types are passed by value, whereas primitive types are passed by reference.