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:
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:
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:
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
:
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:
By applying a const assertion
, you can narrow the type of the array to a tuple with literal types, ensuring immutability:
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:
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 Types | Complex Types | |
---|---|---|
Examples | - Null | - Objects {} |
- Undefined | - Arrays [] | |
- Boolean | - Functions | |
- Number | - User-Defined Objects (e.g., new Car ) | |
- BigInt | ||
- String | ||
- Symbol | ||
Mutability | Immutable | Mutable |
Passed By | Value / Copy | Reference |
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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.