dummy-data
2022/11/132022/11/13

【TypeScript】reaonly不要論

ども、Nashです。

この記事は「TypeScriptのreadonlyについて、状況によっては不要なのではないかという考えについてまとめた記事」になります。

では、見てみます。

TL;DR

  • readonly不要論として、readonly 自体は有用性が高いんだけど使い勝手が微妙なので割り切って入れない選択をしてもいいのでは、という考え方。
  • 具体的には、チーム・プロダクトがそれなりに成長してる状態でないなら意図的に入れない選択をしてもいいかと思う。

背景

いままでreadonly をあまり使ってこなかったが、「これってどんだけ有用なんだろう」と思って調べてみた。

利用箇所としては、関数の引数で配列orオブジェクトを受け取るときに破壊的な変更をさせたくないのでreadonlyを設定する。例えばsortは破壊的な変更をする関数のため関数内部で使っていて気付いたら配列を破壊してることもあるので。

const accending = (
-   list: number[]
+   list : readonly number[]
): number => {
-    return list.sort((a, b) => a-b);
+    return [...list].sort((a, b) => a-b);
}

TS Playground

readonly の問題点

引数にreadonlyを設定すると関数内部で破壊的変更が起きないのでより安全なコードになる。

これだけ見ると導入するほうがいいのでは?とも思うが個人的に考えるTSのreadonlyの問題点を整理してみた。

(1) mutable first な設計

TSはデフォルトでmutableになっている点。逆であってほしい。いまからの変更は現実的ではないのはわかるがせめてcompiler に configを導入してほしい

(2) readonly 宣言が冗長

Rustだと可変な場合はmutでTSだと普遍な場合はreadonlyとなる。ただでさえほぼすべての引数に必要となる上に文字数的にもreadonlyって書くのはちょっと長い。

また必要に応じて書き方としてReadonly<T>である必要もあるのだが、どっちにしても長い。

この対策としてはエイリアスを設定してもいいのかなとも思う。

type R<T> = Readonly<T>;

(3) readonly がネストに対応してない

readonlyの設定がネストしたオブジェクトに対応していない。そのため、自前でDeepReadonly<T>を作っておかないといけない。

しかも、DeepReadonly<T> であるべきところでReadonly<T>を使ってしまいそう。これの回避としてデータ構造の内部詳細を意識して型をつけないといけない。漏れをなくすならすべて常にDeepReadonly<T>にすべきで、必要な場所のみをreadonlyを抜くのがいいがコード冗長さに拍車がかかる。

const fn = (obj: Readonly<{ child: { a: number }}>) => {
    obj.child.a = 99;
    return true;
}

const obj1 = { child: { a: 1 } }
fn(obj1)
console.log(obj1.child.a) // => 99

(4) readonly が完璧に動作しないことがある

Object.assign 使うとreadonlyなのに破壊できる。readonlyとは何だったのか。

const log = console.log;

const fn = (obj1: Readonly<{a: number}>) => {
    return Object.assign(obj1, {b: 1});    
}

const input: Readonly<{a: number}> = {a: 1} as const;

log("input is", input);      // => { a: 1 }
log("result is",  fn(input)) // => { a: 1, b: 1 }
log("input is", input)       // => { a: 1, b: 1 }

readonlyのpros/cons

readonly導入におけるpros/consをまとめるとこうなる。

デメリット

  • コードがとてつもなく冗長になる
  • readonly を設定しても破壊できるコードがありえるので100%の担保にならない

メリット

  • 関数のシグネチャから破壊的変更がないことがわかる(100%ではないが)
  • 誤って破壊的変更を入れてしまっていたらTS compiler が検知してくれる

結論

readonlyを入れることでより堅牢になる点は間違いないが、個人的に使い勝手が悪く感じていてメリデメを考えると常に脳死で使うべきでもないようにも感じた。

得られるメリットももちろんあるがコードの冗長性などのデメリットを考えると、チームやプロダクトの成長具合に応じては「意図的にreadonlyは積極的に使わない」という運用をするのもいいかと思う。

感想

TS の言語設計の敗北なのか、負債で仕方がないのか、将来に解決する事案なのか判断がつかないがとりあえず「やっぱRustってすげーよな」って改めて思った。

Nash
Nash
ソフトウェアエンジニア。国際決済金融SE⇒ITベンチャー⇒フリーランス。日本を出て、海外で働いて、最終ゴールは月で生活すること。
📚おすすめ書籍
🔰全エンジニアの必読本
🛌寝る前のおすすめ本
🎉毎年1つ新しい言語を学ぶ
💡他の記事を読む