【TS】そんなのあったの?PropertyKey型
こんにちは。
最近TypeChallengeにはまっており、「完全に理解した」状態になるまで1問1問に深く向き合っている日々の筆者です。
そんな僕が、長年(3ヶ月くらい)めちゃくちゃストレス(肌寒いなぁくらい。)に思っていることがあり、日々調査に明け暮れ(たまーに5分検索するくらい)ても解決できない問題がありました...。
今回は、そんなストレスを解消することができたので、記事として残したいと思います!
オブジェクトのKeyを指す型
本題です。
util関数を作る際、「何らかのオブジェクトと、そのKeyを受け取って、なんやかんやする関数」を作りたいと思ったことはないでしょうか?(以下の様なイメージですね...。)
const awesomeFunction = (obj: Obj、key: Key) => {
const targetValue = obj[key]
// ...
// なんやかんや
// ...
}
awesomeFunction({hoge: "xxx"}, "hoge")
この時、awesomeFunction
が受け取る型を以下の様に縛りたいと思うでしょう。
-
Obj
: 何らかのオブジェクト。 -
Key
: 受け取ったオブジェクトのkey。
では、ジェネリクスを使用して、受け取る引数に制約を加えていきましょう...。
const awesomeFunction = <Obj, Key extends keyof Obj>(obj: Obj、key: Key) => {
// ...
}
// 👍 型エラーはなく、keyの"hoge"もサジェストしてくれる!!
awesomeFunction({hoge: "xxx"}, "hoge")
// 👍 型エラーになってくれる!!
awesomeFunction({hoge: "xxx"}, "fuga") // 🚫 type error
これで思った通りの挙動になってくれました!!
...本当に...🤔?
思った様にならない...。
実はこのコードは思った様になっていません。
Obj
の型を縛っていないことが原因で問題が起こり得ます。
// 👎 型エラーになってくれない。
awesomeFunction("hoge","toString")
// 👎 型エラーになってくれない。
awesomeFunction([],"filter")
当然と言えば当然なのですが、「第一引数に受け取った値と、そのkeyを受け取る。」という中途半端な型制約のせいで上記の様な問題が起こってしまいます。
となるとやるべきは、「第一引数に何らかのオブジェクトのみを受け取る。」と言う型制約をObj
に行うことです。
どうやって型をつける...?
さて、「何らかのオブジェクトのみを受け取る」という制約はどの様に行えばいいでしょうか?
探してみたことがある方は分かるかと思いますが、「オブジェクト」を表す型っぽいものとして、{}
やObject
なんかを試したこともあるのではないでしょうか?
これらにも細かな違いはあるのですが、いずれもうまくいきません。
これらの違いについてはサバイバルTypeScriptがとても理解しやすく図解してくれていますのでそちらを参照してください。
オブジェクトのkeyがとり得る型
オブジェクトはkey
として指定できる値が
string
number
symbol
の3種しかありません。
上記のいずれかをkey
とし、value
にお好きな値を入れることでいつもオブジェクトを宣言していたわけですね。
これを利用し、型づけを強固にしてみましょう。
const awesomeFunction = <Obj extends {[K in (string|number|symbol)]: unknown}, Key extends keyof Obj>(obj: Obj,key: Key) => {
// ...
}
// 👍 型エラーはなく、keyの"hoge"もサジェストしてくれる!!
awesomeFunction({hoge: "xxx"}, "hoge")
// 👍 型エラー
awesomeFunction({hoge: "xxx"}, "fuga") // 🚫 type error
awesomeFunction("hoge","toString") // 🚫 type error
awesomeFunction([],"filter") // 🚫 type error
これで想定通りに動いてくれるようになりました!!
...なんかやだなぁ🤔。
特に[K in (string|number|symbol)]
の部分を見ても、「オブジェクトのkey型」だ!というのがわからない。ということにストレスを感じます...。
PropertyKey
型
さて、そこで登場するのが
タイトルもあるとおり、PropertyKey
型です。どんな型かは一目瞭然、読んで字の如くです。
declare type PropertyKey = string | number | symbol;
string | number | symbol
のユニオン型であり、これはオブジェクトのkeyのとり得る値型ですね。
これが、typescript内でdeclare
されているためimport
することなくどこでも使用することができます。
公式に用意してくれているPropertyKey
型を使用すれば、誰が見てもオブジェクトのkeyであることは一目瞭然となるでしょう。
この型を使ってよりスマートに書き直してみましょう。
const awesomeFunction = <Obj extends Record<PropertyKey, unknown>, Key extends keyof Obj>(obj: Obj,key: Key) => {
// ...
}
オブジェクト型を定義しておいてもいいのかも...?
オブジェクトのkeyの型は定義がありますが、オブジェクトそのものの型は公式に定義がありません。
以下のような型をプロジェクト内で独自に定義しておくのも良いかもしれません。
declare type MyObject = Record<PropertyKey, unknown>
で
const awesomeFunction = <Obj extends MyObject, Key extends keyof Obj>(obj: Obj,key: Key) => {
// ...
}
こういうイメージですかね!
...いいね😺
これで、オブジェクト以外を第一引数に渡すことができなくなり、期待通りに動いてくれるようになりました!!
まとめ
さて、
今回は「PropertyKey
なんていう型を用意してくれてたんだね!!」という発見を共有したいと思い、記事にしてみました!
読んでくださりありがとうございました!!
よきTSライフを!
Discussion
以下のようにkeyof演算子を使うのはどうでしょう?
すみません、読み違えてました!第一引数の方を縛りたいのですね。
いいえー!コメントありがとうございますー!
疑問に思われたことは気軽にコメントしてくださると僕の勉強にもなりますので嬉しいですー😸!!