TypeScript での Object.assign と スプレッド構文の正しい使い分け
TypeScriptにおけるObject.assignとスプレッド構文の使い分け:型の落とし穴に注意!
はじめに
TypeScriptでオブジェクトをマージするとき、Object.assign とスプレッド構文(...)のどちらを使うか、迷ったことはありませんか?
どちらも見た目は似ていますが、型の扱いに大きな違いがあることをご存知でしょうか?
この記事では、「スプレッド構文では型推論が変化することで、意図しない上書きが静かに行われる」問題を紹介しつつ、安全に使い分ける方法を解説します。
1. よくあるスプレッド構文の使い方
例えば、設定オブジェクトにデフォルト値をマージするようなケース:
type Config = {
retry: number
timeout: number
}
const defaultConfig: Config = {
retry: 3,
timeout: 1000,
}
const userConfig = {
timeout: 5000,
}
const mergedConfig = {
...defaultConfig,
...userConfig,
}
このようにスプレッド構文でオブジェクトをマージするのは一般的なパターンです。しかしここに意外な落とし穴があります。
2. 問題点:スプレッド構文は型の上書きを静かに許容する
userConfig に誤った型が入っていた場合、どうなるでしょうか?
const userConfig = {
timeout: '5000', // ← number ではなく string!
}
const mergedConfig = {
...defaultConfig,
...userConfig,
}
このコードはコンパイルエラーになりません。
実際、mergedConfig.timeout の型は string に推論されてしまいます。
mergedConfig.timeout.toFixed(2) // 💥 実行時エラー!
なぜか?
スプレッド構文は後に書かれたオブジェクトの型が優先されるため、defaultConfig の型が timeout: number であっても、userConfig に string を指定すれば、それがそのまま使われます。
3. 解決策:Object.assignを使うと型チェックが強く働く
同じ操作を Object.assign で書くと、型チェックが厳密に行われるようになります。
const mergedConfig = Object.assign({}, defaultConfig, userConfig)
このとき、もし userConfig.timeout に string を指定すると、TypeScriptが即座に型エラーを出してくれます。
// Type 'string' is not assignable to type 'number'.
これは Object.assign が、第一引数の型をベースにして、以降の引数との整合性をチェックするためです。
ただし、これも userConfig に明確な型注釈があってこそ 有効です。
const userConfig: Partial<Config> = {
timeout: 5000,
}
4. より安全に書くには? satisfies 演算子を活用しよう
TypeScript 4.9 以降では satisfies 演算子を使うことで、型を壊さずに安全性を確保できます。
const userConfig = {
timeout: 5000,
} satisfies Partial<Config>
const mergedConfig = {
...defaultConfig,
...userConfig,
}
userConfig に string 型の値を入れようとすると、この時点でエラーになります。
userConfig の内容を型 Partial<Config> に適合させることを保証しつつ、構造は保たれるので非常に安全です。
5. Object.assign も万能ではない:型推論に注意
const merged = Object.assign({}, defaultConfig, {
timeout: '5000' // string!
})
このように Object.assign に直接リテラルを渡した場合、merged の型が明示されていないと型エラーが出ないケースもあります。
👉 より安全にしたい場合は、代入先の変数に型アノテーションをつけるか、userConfig に型注釈を与えると良いです。
const merged: Config = Object.assign({}, defaultConfig, userConfig)
6. まとめ:使い分けの指針
| 使用方法 | 型安全性 | 使用感 | 直感的 | 説明 |
|---|---|---|---|---|
| スプレッド構文 | △ | ◎ | ◎ | 柔軟で書きやすく、ネストした構造の合成にも対応。ただし型の整合性に注意が必要。 |
| Object.assign | ◎ | ◯ | △ | 厳密な型チェックが働く。記法がやや冗長で、浅いマージのみ対応。ネスト構造には不向き。 |
| スプレッド構文 + satisfies | ◎ | ◯ | ◎ | スプレッド構文の柔軟さを保ちつつ、型の整合性も保証できる。型注釈が必要でやや複雑。 |
✅ 結論
- 安全性重視なら
Object.assignやsatisfiesを使う - スプレッド構文は便利だが、型の上書きが静かに起きることに注意
- コンパイル時の型チェックを信頼するためには、型アノテーションや型ガードを併用するのが大切
おわりに
スプレッド構文と Object.assign はどちらも便利なツールですが、TypeScript では型の扱いが異なるため、意図しない挙動が起きることがあります。
「なぜこの型エラーが出ないのか?」と感じたときは、まずスプレッド構文の型推論を疑ってみてください。
そして、satisfies や Object.assign を使って、より安全で堅牢なコードを書いていきましょう。
👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻👨💻
Discussion