🤖

[TypeScript] readonly 修飾子 と readonly / ReadOnly / ReadonlyArray の型定義

2023/10/02に公開

自分が知らなかったので備忘録として。

問題

export class Hoge {
  constructor(
    private readonly foo: readonly string[] = [],
    private readonly bar: ReadonlyArray<string> = [],
    private readonly piyo: Readonly<string[]> = [],
  ) {}
  add(_: string) {
    this.foo.push(_);
    this.bar.push(_);
    this.piyo.push(_);
  }
}

addメソッド内のすべての push で TypeScript エラーが出るものの、それ以外は構文として正しい。
上記について見ていく。

解説

readonlyは修飾子と型定義につけるものがある。
一見すると同じことをしているように見えるが、違うもの。

1. readonly foo

readonly 修飾子。fooが読み取り専用であることを意図している。
結果、fooは書き換えられない変数になる。

2. foo: readonly string[]

readonly 型定義。fooが読み取り専用配列であることを意図している。
結果、fooは書き換えられない配列になる。

3. bar: ReadonlyArray<string>

ReadonlyArray 型定義。barが読み取り専用配列であることを意図している。
結果、barは書き換えられない配列になる。
※ 2.と同じ

4. piyo: Readonly<string[]>

Readonly 型定義。
string[] に対する定義なので、piyoが読み取り専用配列であることを意図している。
結果、piyoは書き換えられない配列になる。
※ 2.と同じ

ベストプラクティス

readonlyのほうがスペルが短くスッキリして見える印象だが、前述のように同じ綴りでも記述する場所によって効果が違う。
これは開発者がそれに脳内メモリを割かれることにつながるし、
開発現場でありがちなEntityやValueObjectのコピペにおいて、編集ミスで想定外の構成になることもあり得る。

個人的な意見としては、ReadonlyReadonlyArray を正しく使って明確にわかるようにするほうが良いと思っている。

  • readonlyは禁止
  • 配列やタプルの読み取り専用化はReadonlyArrayを必ず使う
  constructor(private piyo: ReadonlyArray<string>) {}

その他(クラス内でのみpush可能にする)

前述のクラスはreadonlyの説明のためだけに作ったもの。
配列に対する編集が全く出来ないので、ロジックとして実際に使えるシーンは多くない。

下記のように、private で修飾しクラス内で取り扱えるようにしながら、外部に渡すときは ReadonlyArray とするのがベストプラクティスだと思う。

export class Hoge {
  constructor(private piyo: string[]) {}
  get(_: string): ReadonlyArray<string> {
    return this.piyo;
  }
  add(_: string): void {
    this.piyo.push(_);
  }
}

参考

https://typescriptbook.jp/reference/values-types-variables/array/readonly-array#読み取り専用配列の特徴

Discussion