Gemcook Tech Blog
🙆‍♂️

【TS】そんなのあったの?PropertyKey型

2024/06/13に公開3

こんにちは。
最近TypeChallengeにはまっており、「完全に理解した」状態になるまで1問1問に深く向き合っている日々の筆者です。
そんな僕が、長年(3ヶ月くらい)めちゃくちゃストレス(肌寒いなぁくらい。)に思っていることがあり、日々調査に明け暮れ(たまーに5分検索するくらい)ても解決できない問題がありました...。

https://github.com/type-challenges/type-challenges/tree/main

今回は、そんなストレスを解消することができたので、記事として残したいと思います!

オブジェクトの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がとても理解しやすく図解してくれていますのでそちらを参照してください。

https://typescriptbook.jp/reference/values-types-variables/object/difference-among-object-and-object

オブジェクトの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ライフを!

Gemcook Tech Blog
Gemcook Tech Blog

Discussion

snowcrushsnowcrush

以下のようにkeyof演算子を使うのはどうでしょう?

const f = <T extends {}>(obj: T, key: keyof T) => obj[key]
snowcrushsnowcrush

すみません、読み違えてました!第一引数の方を縛りたいのですね。

ひょぷてひょぷて

すみません、

いいえー!コメントありがとうございますー!
疑問に思われたことは気軽にコメントしてくださると僕の勉強にもなりますので嬉しいですー😸!!