7 Min. Lesezeit

TypeScript: const vs readonly - fewer errors through defensive programming

dev
Tasse mit der Aufschrift „Talk <CODE> To Me“ und dampfendem Getränk.

Get more out of TypeScript by understanding the properties and differences of "const", "as const" and "readonly"

... and in this article, you will learn why and howconst, as const and readonly can be applied in practice. We won't just scratch the surface of the topic, but delve into the properties and differences of these keywords right at the heart of TypeScript: In the compiler and the type system - let's go!

What role does the TypeScript compiler play?

To fully understand whatconst, as const and readonly do, we need to understand how the TypeScript compiler works. Broadly speaking, tsc performs 2 steps:

  1. Type checking
  2. Transpiling from TypeScript to JavaScript


There is a stark contrast here to other common programming languages like Java, C# or C++: Strong typing exists in TypeScript only until compile time. At runtime, JavaScript is used. The compiler tries to retain as many JavaScript features as possible. This means thatconst (which is a JavaScript keyword) also applies at runtime (provided the target version of JavaScript supportsconst), but as const and readonly as pure TypeScript keywords only apply during compile time.

Let's take a look at the following TypeScript code:

const colors = ['red', 'green', 'blue']
let colors2 = ['red', 'green', 'blue'] as const
const colors3 = ['red', 'green', 'blue'] as const

This is transpiled (with target: ES2021) into the following JavaScript code:

const colors = ['red', 'green', 'blue']
let colors2 = ['red', 'green', 'blue']
const colors3 = ['red', 'green', 'blue']

We see thatas const thus has pure type-checking functionality and does not affect the produced target code. The same applies toreadonly.

Typing in TypeScript

Typing works in TypeScript significantly differently than, for example, in Java or C#. If no explicit type is specified, the compiler tries to figure out which type is best suited for a variable (or constant). A variable assigned a text is typed as a string:

let str1 = 'Foo'  // Type: string

A constant, on the other hand, is typed as a type with only one value:

const str2 = 'Bar' // Type: "Bar"

This makes sense, as the constant can only take the value "Bar".

What types do the three arrays have?

Now the important question: What about the arrays?

const colors = ['red', 'green', 'blue']            // Type: string[]
let colors2 = ['red', 'green', 'blue'] as const    // Type: readonly["red", "green", "blue"]
const colors3 = ['red', 'green', 'blue'] as const  // Type: readonly["red", "green", "blue"]

For arrays, theconst keyword alone does not affect the type, but only whether it is a variable or a constant:

  • const/let only determines whether we can assign a new value to color/color2 (e.g., a completely new array)
  • as const determines whether the assigned array is readonly (i.e., immutable) or not. The generated type exactly corresponds to the specific array with the 3 color strings as content.

The effects of "const" and "as const"

The most important thing upfront: A "const" array (or object) is mutable! To make it readonly, as we are used to from other programming languages, we must useas const.

Here are the details:

const prevents another expression from being assigned to the constant. The content itself (which is located in memory at another location that the constant merely points to) can be changed, provided it is an array or an object:

const colors = ['red', 'green', 'blue']

colors.push('yellow')          // Ok -> Unintuitively, we can change the array even though it is "const"
colors = ['magenta', 'fuxia']  // Error: Cannot assign to 'colors' because it is a constant.

as const defines a (readonly) type that exactly corresponds to the content of the array:

let colors2 = ['red', 'green', 'blue'] as const

colors2.push('yellow')          // Error: Property 'push' does not exist on type 'readonly ["red", "green", "blue"]'.
colors2 = ['magenta', 'fuxia']  // Error: Type '"magenta"' is not assignable to type '"red"'.

Interestingly, the second error message here indicates a type mismatch. Since colors2 is not a constant but a variable, an assignment is generally possible. However, only if the assigned type is also readonly["red", "green", "blue"]. colors3 has exactly this type, so this assignment could be made, but not the other way around (since colors3 is a constant):

let colors2 = ['red', 'green', 'blue'] as const
const colors3 = ['red', 'green', 'blue'] as const

colors2 = colors3  // Ok
colors3 = colors2  // Error: Cannot assign to 'colors3' because it is a constant.

Also "const" objects are mutable

What applies to arrays also applies to objects. The const-keyword prevents a new value from being assigned to a constant, but the content of the constant can be changed:

const car = {
    color: 'green',
    seats: 5
}
car.seats = 4          // Unintuitive: Here we can change the field "seats" even though myCar is const
console.log(car.seats) // 4

To truly make the object constant, we must useas const:

const car = {
    color: 'green',
    seats: 5
} as const
car.seats = 4 // Cannot assign to 'seats' because it is a read-only property.

Practical tip: readonly

So far, we have not used the readonly keyword. This comes into play when defining functions, where we can use it in the type specification of parameters:

function foo(colors: readonly string[]) {
    colors.push('olive')  // Error: Property 'push' does not exist on type 'readonly string[]'.
}

Another use ofreadonly is in classes. Here we can create readonly fields, which (unlike constants) can be set once in the constructor and then must not be changed:

class Animal {
    readonly name: string

    constructor(name: string) {
        this.name = name
    }

    public setName(name: string) {
        this.name = name   // Error: Cannot assign to 'name' because it is a read-only property.
    }
}

Conclusion

Defensive programming helps to avoid or detect errors early. It is important to know the possibilities for this:

  • const prevents direct assignments, but the content of the constant can be changed if that content has mutability, which is the case for arrays and objects.
  • as const makes the content of a variable constant (readonly) and prevents arrays and objects from having their content changed.
  • readonly is a keyword to mark function parameters and fields in classes as non-modifiable.

Do you also love TypeScript? Then get in touch with us :)

More TypeScript stuff

Speaking of defensive programming: On our YouTube channel, you can learn, for example, how to filter arrays in TypeScript type-safely: