Constant Indexed Accesses
TypeScript 5.5 から、オブジェクトとキーが「実質的な定数」であるときに、インデックスアクセスによって型が絞られるようになりました。
オブジェクトへのインデックスアクセス
オブジェクトの変数obj
のプロパティを取得する方法には、obj.key
のようにプロパティキーを指定して取得する方法がありますが、ほかにもプロパティキーを格納した変数keyVar
を使って、obj[keyVar]
という形で取得することができます。
const obj = {
foo: 'bar'
}
const key: keyof typeof obj = 'foo'
console.log(obj[key])
// "bar"
オブジェクトのキーを表す変数key
に'foo'
を格納し、obj[key]
という書き方によってobj
のfoo
キーに対応するプロパティ'bar'
を取得することができます。
obj.key
によってアクセスしようとした場合には、key
に格納されている'foo'
によるアクセスではなく、'key'
というプロパティキーによって指定しているとみなされ、エラーが発生してしまいます。
const obj = {
foo: 'bar'
}
const key: keyof typeof obj = 'foo'
console.log(obj.key)
Property 'key' does not exist on type '{ foo: string; }'.(2339)
変数key
を使ってobj.key
のようにアクセスすることはできないため、変数によるアクセスをしたい場合にはこのインデックスアクセスが有用ですね。
「実質的な定数」とは
「Control Flow Narrowing for Constant Indexed Accesses」 に関するPR には以下のように書かれています。
With this PR we perform control flow analysis for element access expressions obj[key] where key is a const variable, or a let variable or parameter that is never targeted in an assignment.
obj[key]
のようにアクセスするとき、このkey
がconst
で宣言されているか、let
やパラメータで宣言されているが再代入はされないという場合に、型の絞り込みが働くようになります。
const
で置き換えても問題ない場合に、「実質的な定数」として扱われるわけですね。
インデックスアクセスによる型の絞り込み
TypeScript 5.5 からは「実質的な定数」キーを使ったインデックスアクセスによる型の絞り込みが効くようになりました。
型の絞り込みが行えるようになっていることを TypeScript 5.4 とTypeScript 5.5 とで比較して確認してみます。
TypeScript 5.4 でのインデックスアクセス
まず、TypeScript 5.4 でも型エラーのでない以下のコードを考えてみます。
type Fish = { name?: string , age?: number}
const fishName: keyof Fish = 'name'
const fish: Fish = Math.random() > 0.5 ? { name: 'salmon' } : { age: 5 }
if (typeof fish[fishName] !== 'undefined') {
fish[fishName].toUpperCase()
console.log(fish[fishName])
// "salmon"
}
Fish
というオブジェクトの型はオプショナルなname
プロパティを持っています。
上のコードでは、このname
というキーをconst で宣言した変数fishName
に格納しています。
型の絞り込みが行われるのはif (typeof fish[fishName] !== 'undefined')
のブロック内で、TypeScript 5.4 でも型の絞り込みが行われます。
そのため、undefined
の場合にはアクセスできないはずのtoUpperCase()
を使用しても型エラーが出ていません。
今度はfishName
をlet で宣言してみると、以下のように型エラーが出ます。
type Fish = { name?: string , age?: number}
let fishName: keyof Fish = 'name'
const fish: Fish = Math.random() > 0.5 ? { name: 'salmon' } : { age: 5 }
if (typeof fish[fishName] !== 'undefined') {
fish[fishName].toUpperCase() // 型エラーが発生
console.log(fish[fishName])
// "salmon"
}
Object is possibly 'undefined'.(2532)
let
で宣言した場合には変数に別の値が代入される可能性があるため、if(typeof fish[fishName] !== 'undefined')
のブロック内でもundefined
ではないと保証されていないわけですね。
TypeScript 5.5 でのインデックスアクセス
今度はTypeScript 5.5 で試してみると、型エラーが出なくなります。
let
で宣言した変数に再代入が行われていないことをコンパイラが検知できるようになったようですね。
型の絞り込みが行われるのは「実質的な定数」のときですので、別の値が代入されたときには絞り込みが行われずにTypeScript 5.4 のときと同様に型エラーが発生するようになっています。
type Fish = { name?: string; age?: number }
let fishName: keyof Fish = "name"
fishName = 'age'
const fish: Fish =
Math.random() > 0.5 ? { name: "salmon" } : { name: undefined }
if (typeof fish[fishName] !== "undefined") {
fish[fishName].toUpperCase()
//Object is possibly 'undefined'.ts(2532)
//Property 'toUpperCase' does not exist on type 'number'.ts(2339)
console.log(fish[fishName])
}
以下のように初期値と同じ"name"
を再代入した場合にも確かに型エラーが発生します。
(先の例とは異なり、toUpperCase()
に関する型エラーは出なくなっています。)
type Fish = { name?: string; age?: number }
let fishName: keyof Fish = "name"
fishName = "name"
const fish: Fish =
Math.random() > 0.5 ? { name: "salmon" } : { name: undefined }
if (typeof fish[fishName] !== "undefined") {
fish[fishName].toUpperCase()
//Object is possibly 'undefined'.ts(2532)
console.log(fish[fishName])
}
TypeScript 5.5 により、ユーザー定義型ガードなどで型の制御が必要となっていた場面が減るのは嬉しい限りですね。