「型安全なオブジェクト更新」とは何を守ることか
まずゴールをはっきりさせます。
型安全なオブジェクト更新とは、「オブジェクトを書き換えるときに、存在しないプロパティ名を書いたり、間違った型の値を入れたりしないよう、型で守られた状態で更新すること」です。
JavaScriptだと、こういうミスがそのまま通ってしまいます。
const user = { name: "Taro", age: 20 };
user.nmae = "Jiro"; // スペルミス
user.age = "twenty"; // 型のミス
TypeScriptTypeScript では、これをコンパイル時に止めたい。
そのために「オブジェクトの型」と「更新の仕方」をちゃんと設計していく、という話です。
まずは基本:型付きオブジェクトの単純な更新
プロパティの更新は型チェックされる
シンプルな例からいきます。
type User = {
name: string;
age: number;
};
let user: User = {
name: "Taro",
age: 20,
};
user.name = "Jiro"; // OK
user.age = 21; // OK
user.age = "21"; // エラー: Type 'string' is not assignable to type 'number'.
user.nmae = "Ken"; // エラー: Property 'nmae' does not exist on type 'User'.
TypeScriptここで起きていることは直感的です。
name は string だから string しか入れられない。
age は number だから number しか入れられない。
User に存在しないプロパティ名を書いたらエラー。
つまり、「型をちゃんと定義しておけば、プロパティ更新の時点でかなり守られる」ということです。
スプレッド構文で「新しいオブジェクトとして更新」する
破壊的更新と非破壊的更新
さっきのように user.name = ... と書くのは「元のオブジェクトを書き換える」更新です。
一方で、React などではよく「元のオブジェクトはそのままにして、新しいオブジェクトを作る」書き方をします。
const user: User = {
name: "Taro",
age: 20,
};
const updated = {
...user,
name: "Jiro",
};
TypeScriptここで updated の型は自動的に User になります。
スプレッド構文は「元のオブジェクトのプロパティを全部コピーして、一部を上書きした新しいオブジェクトを作る」イメージです。
このときも、型安全はちゃんと効いています。
const bad = {
...user,
age: "21", // エラー: Type 'string' is not assignable to type 'number'.
};
TypeScriptスプレッド構文を使うときのポイントは、「元のオブジェクトに型が付いていれば、その型を引き継いだまま更新できる」ということです。
更新用のヘルパー関数を作ると、もっと安全になる
部分更新を型で表現する
よくあるのが、「User の一部だけを更新したい」というケースです。
そのたびにスプレッドを書くのもいいですが、ヘルパー関数にすると意図がはっきりします。
type User = {
name: string;
age: number;
};
function updateUser(user: User, patch: Partial<User>): User {
return { ...user, ...patch };
}
TypeScriptここで Partial<User> は、「User のすべてのプロパティを optional にした型」です。
つまり、patch には name だけでも age だけでも、両方でも渡せます。
const user: User = { name: "Taro", age: 20 };
const u1 = updateUser(user, { name: "Jiro" }); // OK
const u2 = updateUser(user, { age: 21 }); // OK
const u3 = updateUser(user, { age: "21" }); // エラー
const u4 = updateUser(user, { nmae: "Ken" }); // エラー
TypeScriptここで守られているのは二つです。
一つは、「User に存在するプロパティしか更新できない」こと。
もう一つは、「そのプロパティの型に合う値しか渡せない」こと。
「更新の窓口を関数にして、その引数に型を付ける」
これだけで、オブジェクト更新の安全性はかなり上がります。
readonly と「更新しない」ことを型で表現する
そもそも変えてはいけないものは、更新できないようにする
型安全な更新の一つの極端な形が、「更新させない」です。
ID や作成日時など、「変わるべきではない」プロパティは readonly にしておきます。
type User = {
readonly id: number;
name: string;
};
let user: User = {
id: 1,
name: "Taro",
};
user.name = "Jiro"; // OK
user.id = 2; // エラー: Cannot assign to 'id' because it is a read-only property.
TypeScriptさらに、「この関数はオブジェクトを絶対に書き換えない」と宣言したいときは、Readonly<T> を使います。
function printUser(user: Readonly<User>) {
console.log(user.name);
// user.name = "X"; // エラー
}
TypeScript「ここは変えていい」「ここは変えちゃダメ」を型で表現すること自体が、型安全な更新の一部です。
ネストしたオブジェクトの更新を型安全にやる
ネストしていても「型に沿って」更新する
少しだけ複雑な例にします。
type Address = {
city: string;
zip: string;
};
type User = {
name: string;
address: Address;
};
const user: User = {
name: "Taro",
address: {
city: "Tokyo",
zip: "100-0001",
},
};
TypeScriptcity だけ変えたいとき、よくある書き方はこうです。
const updated: User = {
...user,
address: {
...user.address,
city: "Osaka",
},
};
TypeScriptここでも、型がちゃんと効いています。
zip を number にしようとするとエラーになります。
const bad: User = {
...user,
address: {
...user.address,
zip: 12345, // エラー: Type 'number' is not assignable to type 'string'.
},
};
TypeScriptネストが深くなっても、「型を定義しておく → スプレッドで更新する → 代入先に型を付ける」
この流れを守れば、型安全は維持できます。
まとめ:型安全なオブジェクト更新の「考え方」
テクニックはいくつかありますが、根っこにある考え方はシンプルです。
オブジェクトの「形」と「プロパティの型」を、まずきちんと型として定義する。
その型を守るように、更新の仕方を設計する。
更新の入口(代入先の変数や更新関数の引数)に、必ずその型をぶら下げておく。
そうすると、TypeScript がこういうことを全部チェックしてくれます。
存在しないプロパティを更新していないか。
間違った型の値を入れていないか。
本来変わるべきでないものを変えようとしていないか。
「型に沿ってしか更新できない」状態を作ること。
それが、型安全なオブジェクト更新の本質です。
