スプレッド構文ってそもそも何をしているのか
スプレッド構文(...)は、「あるオブジェクト(や配列)の中身を“広げて”新しいオブジェクト(や配列)を作る記法」です。
const user = { name: "Taro", age: 20 };
const copied = { ...user };
// { name: "Taro", age: 20 }
const updated = { ...user, age: 21 };
// { name: "Taro", age: 21 }
TypeScript{ ...user } は「user のプロパティを全部コピーした新しいオブジェクト」を作ります。{ ...user, age: 21 } は「まず user をコピーして、そのあと age を 21 で上書きしたオブジェクト」を作ります。
ここまでは JavaScript の話ですが、TypeScript では「このスプレッドで作られたオブジェクトに、どんな型が付くか」が重要になってきます。
オブジェクトのスプレッドと型推論の基本
単純なコピーの場合の型推論
type User = {
name: string;
age: number;
};
const user: User = {
name: "Taro",
age: 20,
};
const copied = { ...user };
TypeScriptこのとき copied の型は、ほぼそのまま User と同じになります。
// 推論されるイメージ
const copied: {
name: string;
age: number;
}
TypeScriptつまり、copied.name は string、copied.age は number として扱われます。
元のオブジェクトに型が付いていれば、スプレッドで作ったオブジェクトにも、その型情報が引き継がれるイメージです。
上書きしたときの型チェック
const updated = {
...user,
age: 21,
};
updated.age = 30; // OK(number)
updated.age = "30"; // エラー
TypeScriptupdated も { name: string; age: number } と推論されるので、age に string を入れようとすると、ちゃんとコンパイルエラーになります。
ポイントは、「スプレッドで作ったオブジェクトも、普通のオブジェクトと同じように型チェックされる」ということです。
スプレッドで「型安全な更新」をする
元のオブジェクトを壊さずに更新する
React などでよく見る「イミュータブルな更新」は、TypeScript と相性がいいです。
type User = {
name: string;
age: number;
};
const user: User = {
name: "Taro",
age: 20,
};
const updated: User = {
...user,
age: 21,
};
TypeScriptここでは、
userはそのままupdatedは「age だけ変えた新しい User」
という関係になります。
代入先に : User と型を付けておくことで、
「User に存在しないプロパティを書いていないか」「型が合っているか」を再チェックしてくれます。
const bad: User = {
...user,
age: "21", // エラー
nmae: "Ken", // エラー(スペルミス)
};
TypeScript「スプレッドで更新するときも、最後に“どの型として扱うか”をはっきりさせる」
これが、型安全なオブジェクト更新のコツです。
複数オブジェクトをスプレッドしたときの型の合成
2つのオブジェクトをマージする
type User = {
name: string;
};
type Info = {
age: number;
};
const user: User = { name: "Taro" };
const info: Info = { age: 20 };
const merged = { ...user, ...info };
TypeScriptこのとき merged の型は、ざっくりこうなります。
// 推論イメージ
{
name: string;
age: number;
}
TypeScriptTypeScript は、「スプレッドされたオブジェクトたちのプロパティを全部集めた型」を作ります。
もし同じプロパティ名があれば、後ろのものが上書きされます。
const a = { x: 1, y: 2 };
const b = { y: 3, z: 4 };
const c = { ...a, ...b };
// c の型: { x: number; y: number; z: number }
// 実際の値: { x: 1, y: 3, z: 4 }
TypeScript型としても、「y は number」として扱われます。
「どのプロパティが最終的に残るか」は、スプレッドの順番に依存することを意識しておくと、挙動が読みやすくなります。
optionalプロパティとスプレッドの型
optional が混ざるときのイメージ
type Base = {
id: number;
name?: string;
};
const base: Base = { id: 1 };
const extended = {
...base,
name: "Taro",
};
TypeScriptextended の型は、id: number; name?: string のような形になりますが、
実際の値としては name が存在しています。
ここで大事なのは、「optional かどうかは“型の話”であって、スプレッドした瞬間に必ずしも変わるわけではない」ということです。
もう一つ、よくあるパターンを見てみます。
type Required = {
value: number;
};
type Optional = {
value?: number;
};
function merge(a: Required, b: Optional) {
return { ...a, ...b };
}
TypeScriptこのとき戻り値の型は、実験すると「value: number のまま」と推論されます。
つまり、「Optional 側で value? になっていても、Required の value: number がベースとして残る」イメージです。
ここから分かるのは、「スプレッドの型推論は完璧に“現実の全パターン”を表してくれるわけではない」ということです。
ただ、初心者のうちは「基本的には“全部マージされたオブジェクトの型”になる」と押さえておけば十分です。
readonly や as const とスプレッド
readonly なオブジェクトをスプレッドするとどうなるか
type User = {
readonly id: number;
name: string;
};
const user: User = {
id: 1,
name: "Taro",
};
const copied = { ...user };
TypeScriptcopied のプロパティは、通常の推論だと readonly ではなくなります。
つまり、copied.id は書き換え可能な number として扱われることが多いです。
「絶対に変えたくない設定値」などをスプレッドで扱うときは、as const や Readonly<T> を組み合わせて、どこまで不変にするかを意識して設計する必要があります。
スプレッド構文と型を扱うときの考え方
スプレッド構文そのものはシンプルです。
でも、型と組み合わさるときに意識しておきたいポイントは、こんな感じです。
元のオブジェクトに型を付けておくと、スプレッド後のオブジェクトにもその型情報が引き継がれる。
スプレッドで更新した結果を、どの型として扱うか(代入先の型)をはっきりさせると、安全性が上がる。
複数オブジェクトをスプレッドするときは、「どのプロパティが最終的に残るか」は順番で決まる。
optional や readonly が絡むと、推論結果が直感とズレることもあるので、「完璧なモデル」ではなく「だいたいこうなる」くらいの感覚で捉える。
そして一番大事なのは、
「スプレッドは“新しいオブジェクトを作るための道具”であり、型は“その新しいオブジェクトの形を保証するもの”」
この二つをセットで考えることです。
「このスプレッドの結果は、どういう形のデータになるべきか?」
それを型として書き下しておくと、TypeScript がその意図から外れた更新を全部止めてくれます。
