Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 733
Paleo Avatar asked Sep 25 '17 08:09

Paleo


2 Answers

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.

like image 127
Romain Deneau Avatar answered Oct 15 '22 06:10

Romain Deneau


What creates the confusion is the "double" usage of the word `value' in your factory function.

To clarify, rewrite it this way (please note the _ in front of the _value variable):

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
        }
    }
}
  1. _value is just a local var than you can mutate
  2. get value() implements the interface definition readonly value: number: counter.value is readonly
like image 44
Bruno Grieder Avatar answered Oct 15 '22 06:10

Bruno Grieder