Stefan Wöhrer, view all articles
05.09.2022
7 min read
Tasse mit der Aufschrift „Talk <CODE> To Me“ und dampfendem Getränk.

TypeScript: const vs readonly - fewer errors through defensive programming

  • dev
Short summary:

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

... and in this article, you'll learn why and how const, as const and readonly are applicable in practice. We won't just scratch the surface but delve into the properties and differences of these keywords right at the heart of TypeScript: the compiler and the type system - let's go!

What role does the TypeScript compiler play?

To fully understand what const, 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 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 that const (which is a JavaScript keyword) also applies at runtime (provided the target version of JavaScript supports const), but as const and readonly as pure TypeScript keywords only take effect 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 that as const thus has pure type-checking functionality and does not affect the produced target code. The same applies to readonly.

Typing in TypeScript

Typing works significantly differently in TypeScript compared to Java or C#. If no explicit type is provided, the compiler tries to determine which type is best suited for a variable (or constant). A variable assigned a string is typed as 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, the const 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 or not (e.g., a completely new array)
  • as const determines whether the assigned array is readonly (i.e., immutable) or not. The generated type exactly matches the specific array with the 3 color strings as content

The effects of "const" and "as const"

The most important point first: A "const" array (or object) is mutable! To make it readonly, as we are used to from other programming languages, we need to use as const. Here are the details:

const

prevents a different expression from being assigned to the constant. The content itself (which is stored in a different location in memory that the constant merely references) can be changed , provided it is an array or an object:as const

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

colors.push('yellow')          // Ok -> Unintuitiver weise können wir das Array verändern, obwohl es "const" ist
colors = ['magenta', 'fuxia']  // Error: Cannot assign to 'colors' because it is a constant.

defines a (readonly) type that exactly matches the content of the array: The second error message here is interesting: It 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

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"'.

Const objects are also mutable

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.

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:To truly make the object constant, we need to

const car = {
    color: 'green',
    seats: 5
}
car.seats = 4          // Unintuitiv: Hier können wir das Feld "seats" ändern, obwohl myCar const ist
console.log(car.seats) // 4

use as const: Practical tip: readonly So far, we haven't used the readonly keyword. This comes into play when defining functions, where we can use it in the type specification of parameters:

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

Another use of

readonly

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

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: Conclusion Defensive programming helps avoid or detect errors early. It is important to know the possibilities for this:

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.
    }
}

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 the content of arrays and objects from being changed
  • readonly is a keyword to mark function parameters and fields in classes as non-mutable
  • Do you love TypeScript too? Then get in touch with us :) More TypeScript stuff

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

Video: In-depth Tutorial on Type-Safe Array Filtering

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

this is the end my friend

Who wrote it

Stefan Wöhrer CEO & Senior Software Architect

Stefan combines technical expertise with entrepreneurial thinking. As CEO and Senior Software Architect, he shapes systems and strategy, guides projects from conception to implementation, and creates scalable, future-proof solutions with a focus on clear, sustainable structures.