TypeScript | 基礎文法:オブジェクト基礎 – スプレッド構文と型

TypeScript
スポンサーリンク

スプレッド構文ってそもそも何をしているのか

スプレッド構文(...)は、「あるオブジェクト(や配列)の中身を“広げて”新しいオブジェクト(や配列)を作る記法」です。

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"; // エラー
TypeScript

updated{ 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;
}
TypeScript

TypeScript は、「スプレッドされたオブジェクトたちのプロパティを全部集めた型」を作ります。

もし同じプロパティ名があれば、後ろのものが上書きされます。

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",
};
TypeScript

extended の型は、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? になっていても、Requiredvalue: number がベースとして残る」イメージです。

ここから分かるのは、「スプレッドの型推論は完璧に“現実の全パターン”を表してくれるわけではない」ということです。
ただ、初心者のうちは「基本的には“全部マージされたオブジェクトの型”になる」と押さえておけば十分です。


readonly や as const とスプレッド

readonly なオブジェクトをスプレッドするとどうなるか

type User = {
  readonly id: number;
  name: string;
};

const user: User = {
  id: 1,
  name: "Taro",
};

const copied = { ...user };
TypeScript

copied のプロパティは、通常の推論だと readonly ではなくなります。
つまり、copied.id は書き換え可能な number として扱われることが多いです。

「絶対に変えたくない設定値」などをスプレッドで扱うときは、
as constReadonly<T> を組み合わせて、どこまで不変にするかを意識して設計する必要があります。


スプレッド構文と型を扱うときの考え方

スプレッド構文そのものはシンプルです。
でも、型と組み合わさるときに意識しておきたいポイントは、こんな感じです。

元のオブジェクトに型を付けておくと、スプレッド後のオブジェクトにもその型情報が引き継がれる。
スプレッドで更新した結果を、どの型として扱うか(代入先の型)をはっきりさせると、安全性が上がる。
複数オブジェクトをスプレッドするときは、「どのプロパティが最終的に残るか」は順番で決まる。
optional や readonly が絡むと、推論結果が直感とズレることもあるので、「完璧なモデル」ではなく「だいたいこうなる」くらいの感覚で捉える。

そして一番大事なのは、
「スプレッドは“新しいオブジェクトを作るための道具”であり、型は“その新しいオブジェクトの形を保証するもの”」
この二つをセットで考えることです。

「このスプレッドの結果は、どういう形のデータになるべきか?」
それを型として書き下しておくと、TypeScript がその意図から外れた更新を全部止めてくれます。

タイトルとURLをコピーしました