TypeScriptでオブジェクトからundefinedを消す、filterだけだと型は追いつかない

  • TypeScript

この記事は、構成と文章の整理にAIを使っています。
あくまで読みやすくするための補助で、内容そのものをAIにすべて任せて書いているわけではありませんが、ところどころ相談しながら整えています。

オブジェクトからundefinedのプロパティを取り除きたい場面があって、いくつかのやり方を比較してみた。

試した3パターン

1. Object.entries + Object.fromEntries

const cleanObj = Object.fromEntries(
Object.entries(obj).filter(([_, value]) => value !== undefined)
);

配列に変換してフィルタして戻すやつ。元のオブジェクトは変更しないので扱いやすい。

2. for...in + delete

for (const key in obj) {
if (obj[key] === undefined) {
delete obj[key];
}
}

元のオブジェクトを直接書き換える版。

3. ユーティリティ関数化

function removeUndefined<T extends Record<string, any>>(obj: T): Partial<T> {
return Object.fromEntries(
Object.entries(obj).filter(([_, value]) => value !== undefined)
);
}

1をジェネリクスで包んだだけのもの。

実行結果は全部合ってる、でも型が甘い

3つとも実行時の挙動は期待通りになる。問題は型のほう。

filter(([_, value]) => value !== undefined) は普通の絞り込みで、型ガード(value is X のpredicate)になっていない。なのでTypeScript的には cleanObj の値の型に undefined が残ったままになる。動くけど型は嘘をつく状態。

型を正確にしたいなら、こう書く必要がある。

Object.entries(obj).filter(
(entry): entry is [string, Exclude<typeof entry[1], undefined>] => entry[1] !== undefined
)

3のユーティリティ関数の戻り値を Partial<T> にしてるのも、近似としては悪くないけど厳密じゃない。「全プロパティがoptionalになった型」というだけで、実際に残ったプロパティが正しく型付けされる保証はない。1と同じ理由。

for…inのdeleteは要注意

2の for...in + delete は、strictモード(noImplicitAny など)だと obj[key] の部分でコンパイルエラーになる可能性が高い。インデックスシグネチャがない型に文字列でアクセスしてる、という理由。今回のようなリテラル型のオブジェクトだと普通に引っかかるはず(未確認、実機で試したわけではない)。動かすには型アサーションかインデックスシグネチャが必要になりそう。

あと元のオブジェクトを破壊的に変更する点も、使う場所によってはリスクになる。

共通の限界

  • どれもトップレベルのプロパティしか見ていない。ネストしたオブジェクトの中のundefinedは消えない
  • 対象は undefined だけで、null は対象外

型の正確さまで求めるかどうかで、書き方を変えたほうがよさそう。