TypeScript で readonly 配列に push できず Property does not exist になり変更できない
`readonly T[]` / `ReadonlyArray<T>` / `as const` で得た配列はミューテーション系メソッド(push / pop / sort 等)の型を持たないため、変更しようとすると型エラーになります。
スプレッドで可変コピーを作るか、可変な型に再宣言するのが基本対処です。
#readonly#array#as-const#immutable
公開:
要約
Property 'push' does not exist on type 'readonly number[]' のようなエラーは、readonly T[] / ReadonlyArray<T> / as const の配列が変更系メソッドを型レベルで持たないために出ます。
値を変更したい場合はスプレッドや Array.from で可変コピーを作り、それを書き換えるのが基本です。
不変性を保ちたいなら新しい配列を返す関数型の書き方に切り替えます。
よくある原因
- 変数や引数の型を
readonly T[]/ReadonlyArray<T>で宣言している - リテラルに
as constを付けたため、配列がreadonly [...]タプル型になっている - ライブラリ関数が
ReadonlyArray<T>を返しており、その結果に push しようとしている Object.freeze由来で型がreadonly化されている
解決策
1. スプレッドで可変コピーを作る
const nums: readonly number[] = [1, 2, 3];
const mutable = [...nums];
mutable.push(4);Array.from(nums) でも同じ結果が得られます。
元配列の不変性を保ったまま、新しい配列だけ操作できます(公式ハンドブック)。
2. 不変にする意図が無いなら型注釈を変える
// 変更前
const xs: readonly number[] = [1, 2, 3];
xs.push(4); // ERROR
// 変更後
const xs: number[] = [1, 2, 3];
xs.push(4); // OK3. as const を外すか、入れ子だけに使う
const colors = ["red", "green", "blue"] as const;
colors.push("yellow"); // ERROR: as const でタプル化された配列
const colors2 = ["red", "green", "blue"];
colors2.push("yellow"); // OK: string[]
type Color = "red" | "green" | "blue";
const colors3: Color[] = ["red", "green", "blue"];
colors3.push("yellow"); // ERROR: "yellow" は Color ではない4. 関数 API は readonly 受け取り・呼び出し側でコピー
引数を readonly T[] で受け取ると、関数の中で誤って書き換える事故を型で防げます。
書き換えたい場合は呼び出し側で slice() してから渡します。
function append(xs: readonly number[], x: number): number[] {
return [...xs, x];
}
const a = [1, 2, 3];
const b = append(a, 4);不変性を維持しつつ「変更後の配列」を返す関数型の書き方は、Redux / Zustand / Recoil などの状態管理ともよく合います。