水曜日, 8月 13, 2025
水曜日, 8月 13, 2025
- Advertisment -
ホームニューステックニュースTypeScript では T ではなく ReadonlyArray を使おう

TypeScript では T[] ではなく ReadonlyArray を使おう


公開 API では T[] を避け, ReadonlyArray を使いましょう. 内部実装でのみArrayを使うのはOK

以下のコードには受け取った配列をソートして表示する sortLog 関数と, 0番目の要素を 999 にした配列を表示する setLog 関数を定義して使用しています

const sortLog = (array: Arraynumber>): void => {
  console.log(array.sort((a, b) => a - b));
};

const setLog = (array: Arraynumber>): void => {
  array[0] = 999;
  console.log(array);
};

const array = [2, 1, 3];
sortLog(array); 
setLog(array); 
console.log(array); 

Array.prototype.sortarray[index] = value は破壊的変更をする仕様なため, 意図せず呼び出し元の配列を変更してしまいます. これを防ぐために ReadonlyArray を使います

typescript-readonly-array-error

このように Array.prototype.sortarray[index] = value をしようとすると型エラーになり間違いに気づくことができます

破壊的変更をしない代わりのメソッドを使うと以下のようになります

const sortLog = (array: ReadonlyArraynumber>): void => {
  console.log(array.toSorted((a, b) => a - b));
};

const setLog = (array: ReadonlyArraynumber>): void => {
  console.log(array.with(0, 999));
};

const array: ReadonlyArraynumber> = [2, 1, 3];
sortLog(array); 
setLog(array); 
console.log(array); 

React のコンポーネントの Props など破壊的変更をするを意図していないのなら必ず ReadonlyArray で指定することを強くおすすめします. そうすることによってArray を「破壊的変更を意図している」という意味で使うことができます

  • Deno KV の Deno.KvKey
  • GraphQL サーバーの実装にほぼ使うであろう, 取得をまとめてするためのライブラリ dataloader の DataLoader 定義に使う keys
    dataloader-keys-type.png
  • サーバーレスデータベースのxata liteのTypeScript SDK のIDの配列から一度に複数取得するreadメソッド https://github.com/xataio/client-ts/issues/1286 (私が提案しました)

などで使われています

また, ReadonlyArray の方が必要なメソッドが少ないため, Array をそのまま受け取ることもできます

const sortLog = (array: ReadonlyArraynumber>): void => {
  console.log(array.toSorted((a, b) => a - b));
};

const array: Arraynumber> = [2, 1, 3];

sortLog(array);     
console.log(array); 

ほとんどの場合 ReadonlyArray を使えばOKです

Array の方が分かりやすい例

直列でHTTP APIを呼ぶ例です

const apiGenerator = async function* (): AsyncGeneratorstring, void, unknown> {
  for (const v of Array.from({ length: 3 }, (_, i) => i)) {
    const url = new URL("https://postman-echo.com/get");
    url.searchParams.set("v", `${v}`);
    yield (await (await fetch(url)).json()).args.v;
  }
};

const callApis = async (): PromiseReadonlyArraystring>> =>
  await Array.fromAsync(apiGenerator());

console.log(await callApis()); 

Async Generator を作るのが面倒なのも分かるので, Array の変数の範囲が関数内に収まるのなら このようにArray を使っても良いと思います

const callApis = async (): PromiseReadonlyArraystring>> => {
  const result: Arraystring> = [];
  for (const v of Array.from({ length: 3 }, (_, i) => i)) {
    const url = new URL("https://postman-echo.com/get");
    url.searchParams.set("v", `${v}`);
    result.push((await (await fetch(url)).json()).args.v);
  }
  return result;
};

console.log(await callApis()); 

ReadonlyArray と readonly T[] は意味は同じですが, ReadonlyArray の方が以下の理由で好んで使っています

  • T[] という配列のための専用構文ではなく, 他のジェネリックを使った指定と揃えることができる
  • 入れ子になっていても分かりやすい
    type OuterA = readonly number[][];
    type OuterB = readonly (number[])[];
    type OuterC = ReadonlyArrayArraynumber>>;
    
    type InnerA = (readonly number[])[];
    type InnerB = ArrayReadonlyArraynumber>>;
    

    OuterA, OuterB, OuterC は外側の配列が読み取り専用で, 内側は変更可能
    InnerA, InnerB は外側の配列が変更可能で, 内側は読み取り専用

  • VSCodeでCtrl+クリックなどでできる「定義への移動」でメソッドの一覧を見ることができる

要素数が事前に決まっている場合は readonly の記法しか使えないため 仕方ないですが readonly の記法で書きましょう

type Position = readonly [number, number, number];

const a: Position = [1, 2, 3];

ReadonlyArray の他にも TypeScript の標準ライブラリには ReadonlySet, ReadonlyMap があります. 積極的に使いましょう

const readonlySet: ReadonlySetstring> = new Set(["C", "B"]);
console.log(readonlySet.isSubsetOf(new Set(["A", "B", "C"]))); 
const newSet: ReadonlySetstring> = new Set([...readonlySet, "D"]);
console.log(newSet); 
console.log(readonlySet); 
const readonlyMap: ReadonlyMapnumber, string> = new Map([
  [1, "A"],
  [2, "B"],
  [3, "C"],
]);
console.log(readonlyMap.get(2)); 
const newMap: ReadonlyMapnumber, string> = new Map([...readonlyMap, [
  2,
  "BB",
]]);
console.log(newMap); 
console.log(readonlyMap); 

ReadonlyArray に比べて非破壊的に操作するメソッドがないため. 色々操作するときはスコープを関数中に収めて Set Map を使いましょう

  • Array に対応する ReadonlyArray
  • Set に対応する ReadonlySet
  • Map に対応する ReadonlyMap

があるなら

  • Uint8Array に対応する ReadonlyUint8Array
  • URL に対応する ReadonlyURL

などがあっても良いと思われますが, この Issue に書かれているように TypeScript の標準ライブラリには含めない方針のようです

https://github.com/microsoft/TypeScript/issues/37792


代わりに 私が JSRパッケージを作ったので良かったら使ってくださいね

https://jsr.io/@narumincho/readonly

他のReadonlyの型は適宜追加しようと思います. Issue, Pull Request 歓迎です

オブジェクトのプロパティに対しても readonly を指定することができます

type Account = {
  readonly id: string;
  readonly name: string;
};

const account: Account = {
  id: crypto.randomUUID(),
  name: "A",
};

account.name = "B"; 

できるだけ readonly を指定する方が良いと思います. タイプ数を少しでも減らしたい人は Readonly を使うこともできますが,

  • React Props で使ったとき, StoryBook で型が解釈できないため Controlsの表示が一部なくなる
  • 再帰的には適用されない

ことに注意が必要です

私の個人のプロジェクトでは読み取り専用としか使わない場合は, 全部 readonly をつけています. 私は少しでも型安全性が上がるならタイプ数が増えても良いと考えていますが, タイプ数や余計な修飾子を付けたくない人もいるようです

Object.freeze

Object.freeze を使って型チェック時だけでなく, 実行時にも読み取り不可にすることもできます

type Account = {
  readonly id: string;
  readonly name: string;
};

const account: Account = Object.freeze({
  id: crypto.randomUUID(),
  name: "A",
});

account.name = "B"; 

有名な読み取り専用のプロパティは window.undefined ですね

ただ あまり使われないため V8 などの JavaScript 実行エンジンの最適化が発揮されず遅くなることがあります. readonly を使った型チェック時だけでも充分バグを見つけられるため Object.freeze を使うことは少ないでしょう

JavaScript Records & Tuples Proposal (撤回)

「JavaScript Records & Tuples Proposal」という提案がありましたが 今年 2025年に撤回されました

https://github.com/tc39/proposal-record-tuple/blob/d19ccc0372cb7140e6a9b7a010f6219233e552f1/README.md#L108-L127

https://github.com/tc39/proposal-record-tuple/issues/394

デフォルトで読み取り専用になるステキな提案でしたが, 新たに構文とプリミティブ型を追加するのはとても大変なので撤回されたのは仕方ないと思います

proposal Composites (Stage 1)

https://github.com/tc39/proposal-composites/blob/ae5ea98e7c966581f46af37e80f954335ad78948/README.md#L72-L80

https://github.com/tc39/proposal-composites

Object.freeze のようにオブジェクトをつくってから読み取り専用にするアプローチ. 例で挙げられているようにSet, Map のキーでの活用が進みそう. それ以外のオブジェクトでは, 実行エンジンの最適化とTypeScriptが対応すれば使われるようになると思います

デフォルトが変更可能になってしまったTypeScriptで, 「Readonlyを使おう」という啓蒙活動をするよりも, デフォルトで読み取り専用になっているRustなどの言語の啓蒙活動のほうが, 各個人が覚えることが少なく 間違うことも減り良い気もする

似たような主張の記事

https://azukiazusa.dev/blog/q-typescript-readonly-shorts/

諦めて readonly を使わない主張の記事

https://zenn.dev/snamiki1212/scraps/9006206a583a70



Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -