Search code examples
typescriptreadonly

Usages of "readonly" in TypeScript


Usage 1: a function declares its parameter won't be modified

This usage is very simple: as a contract, the function doSomething declares it doesn't mutate the received parameter.

interface Counter {
  name: string
  value: number
}

function doSomething(c: Readonly<Counter>) {
  // ...
}

let c = {
  name: "abc",
  value: 123
}
doSomething(c)
// Here we are sure that 'c.name' is "abc" and 'c.value' is '123'

Usage 2: a factory declares that its output cannot be modified

With this code:

interface Counter {
  readonly name: string
  readonly value: number
  inc(): number
}

function counterFactory(name: string): Counter {
  let value = 0
  return {
    get name() {
      return name
    },
    get value() {
      return value
    },
    inc() {
      return ++value
    }
  }
}

We have here a member readonly value that cannot be modified directly from the outside. But a member inc() can mutate the value. Also, the member value is declared as readonly but its value is changing.

I would like to know if this use of readonly on the member value is a good way to proceed. The syntax is OK. But is this example semantically correct in TypeScript? Is that what the modifier readonly is for?


Solution

  • The readonly keyword on a property does not ensure that the property value is constant. There's no TypeScript equivalent for that. The only things we can be sure with a readonly property are:

    1. Its value can't be changed from the consumer side → usage 1.
    2. It can be the inferred type of a get-only property → usage 2. Note that, if the counterFactory return type is not defined, it is inferred exactly like the Counter interface, see (A) in the code below.
    3. Its value can be set only once and only during the object construction → see (B) below.

    Code example:

    // (A) Usage 2 using type inference
    const counter = counterFactory('foo');
    type Counter = typeof counter; // Produce the exact same type as the previous `Counter` interface
    counter.value = 10; // [Ts Error] Can not assign to 'value' because it is a constant or read-only property
    
    // (B) Readonly property initialization
    // (B1) With an object literal + interface 
    const manualCounter: Counter = { name: 'bar', value: 2, inc: () => 0 };
    manualCounter.value = 3; // [Ts Error] Can not assign...
    
    // (B2) With a class
    class Foo {
      constructor(public name: string, public readonly value: number) { }
      inc() {
        return ++this.value; // [Ts Error] Can not assign...
      }
    }
    
    const foo = new Foo('bar', 3);
    foo.value = 4; // [Ts Error] Can not assign...
    
    // (C) Circumvent TypeScript
    Object.assign(foo, { value: 4 }); // → No error neither in Ts, nor at runtime
    

    It's really confusing because it's almost like a constant property! The usage 2 and the case C prove it's not.