TypeScript 4.9 で in 演算子による型の絞り込みが改善された
TypeScript において `in` 演算子を `unknown` 型に対して使用した際の挙動が改善されました。
TypeScript は if 文などの制御フロー分析することで、自動的に型を絞り込んでくれます。この仕組みは「型ガード」と呼ばれます。
型を絞り込む方法はいくつかありますが、その中でも in 演算子を使用した方法が存在します。in 演算子は左辺で指定されたプロパティが右辺で指定したオブジェクトに存在する場合に true を返します。この性質を利用して TypeScript では検査したプロパティを持つ型のみに絞り込まれます。
type User = {
id: number
name: string
}
type Admin = {
id: number
name: string
role: string
}
const doSomething = (x: User | Admin) => {
if ('role' in x) {
x
// ^?(parameter) x: Admin
} else {
x
// ^?(parameter) x: User
}
}関数 doSomething の引数には User または Admin 型が渡されます。if ('role7 in x) のように in 演算子を用いて条件分岐を行ったとき、true の際のブロックには role プロパティを持つ Admin 型しか存在し得ないのは明白です。そのため、TypeScript により Admin 型に推論されるわけです。
このように in 演算子は正しく使用すれば有益なのですが、unknown 型に対して in 演算子を用いる際に 1 つの問題が生じていました。以下の例を見てみましょう。
type User = {
id: number
name: string
}
const isUser = (x: unknown): x is User => {
if (
typeof x === 'object' &&
x !== null &&
'id' in x &&
typeof x.id === 'number' && // Property 'id' does not exist on type 'object'.
'name' in x &&
typeof x.name === 'string' // Property 'string' does not exist on type 'object'.
) {
return true
} else {
return false
}
}isUser 関数はある不明な型が User 型かどうか判定するための関数です。例えば API から返却されたオブジェクトを安全に使用したい場合などにこのような関数が使われることがあるでしょう。
ここで問題になっているのは typeof x.id で id が number 型かどうか判定している箇所です。このコードは && 演算子により「x がオブジェクト型である」「x は null ではない」「x に id プロパティが存在している」ことが保証されているはずです。しかし「Property 'id' does not exist on type 'object'」と言われているとおり、in 演算子を用いているにも関わらず id プロパティが存在しないとエラーを報告しています。
これは x の型が unknown から object に絞られている一方 in 演算子はチェック対象のプロパティを実際に定義している型に厳密に絞られるためです。結果的に x は object 型のままとなってしまいます。この問題を解決するためには any 型を使うほかありませんでした。しかし、any 型はできる限り使用を避けないものです。
TypeScript 4.9 からはこの挙動が改善されました。in 演算子が使われた場合 Record<"id", unknown> との交差型となるように改善されました。
type User = {
id: number
name: string
}
const isUser = (x: unknown): x is User => {
if (
typeof x === 'object' &&
x !== null &&
'id' in x &&
typeof x.id === 'number' && // object & Record<"id", unknown>
'name' in x &&
typeof x.name === 'string' // object & Record<"id", unknown> & Record<"name", unknown>
) {
return true
} else {
return false
}
}