8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【TypeScript】is keyof を使用してオブジェクトのプロパティを持っているか判定する

Last updated at Posted at 2022-03-05

code

interface Post {
  id: number
  content: string
  createdAt: Date
}

const post: Post = {
  id: 1,
  content: 'textContent',
  createdAt: new Date(),
}

const keys = Object.keys(post)
const checkKey: string = 'id'

if (keys.includes(checkKey)) {
  console.log(post[checkKey])  // エラー①
}

if (['id', 'content', 'createdAt'].includes(checkKey)) {
  console.log(post[checkKey])  // エラー②
}

if ('id' === checkKey) {
  console.log(post[checkKey])  // 成功③
}

const checkPostType = (
  attribute: string,
  keys: string[]
): attribute is keyof Post => {
  return keys.includes(attribute)
}

if (checkPostType(checkKey, keys)) {
  console.log(post[checkKey])  // 成功④
}

初めに

TypeScriptを書いていると特定のオブジェクトに対して動的なKeyで値を取得したいケースがありました
改めて型ガードについて調べた時に良い感じに書けたので載せます

また、上記に載せているコードを元に話を進めていきます

コードについて

ここではアクセスするオブジェクトをpostとして設定し、型としてPostを設定します。
動的なkeyの役割としてcheckKeyに対してpostの属性であるidを明示的に設定してますが、実際には何が入るかわからないため、型はstringにしています

後はこれでできそうな方法を4つ載せてそれぞれについて見ていきましょう

エラー① keys.includes(checkKey)

const keys = Object.keys(post)
const checkKey: string = 'id'

if (keys.includes(checkKey)) {
  console.log(post[checkKey])  // エラー①
}

keysには対象のpostのプロパティ名一覧が入り、それにcheckKeyが含まれているかをチェックするので
最初にこれが思い浮かびましたが残念ながらエラーとなります。

エラー② ['id', 'content', 'createdAt'].includes(checkKey)

const keys = Object.keys(post)
const checkKey: string = 'id'

if (['id', 'content', 'createdAt'].includes(checkKey)) {
  console.log(post[checkKey])  // エラー②
}

次はkeysの箇所を明示的に指定しました。
見るに堪えないコードかもしれませんが一度試してみましたが、ダメでした。

エラー①も同様ですが、ここでチェックしても条件分岐の内部でcheckKeyの型がstringとして判定されているのが原因です

成功③ 'id' === checkKey

const keys = Object.keys(post)
const checkKey: string = 'id'

if ('id' === checkKey) {
  console.log(post[checkKey])  // 成功③
}

次はkeysの箇所を明示的に指定しました。
先ほどまではcheckKeyがstring型として判定されましたが、明確にidと比較することで
checkKey: idというリテラル型として認識されpostのプロパティに存在するidと同一であることが保証されて成功したようです。

ただ、この書き方だと「textContentやcreatedAtなど他のプロパティを取るとき」や「Post型のメンバーが増えた時」に変更が加わるので好ましくはないですね

// 好ましくない例
if ('id' === checkKey || 'textContent' === checkKey || 'createdAt' === checkKey) {
  console.log(post[checkKey])
}

成功④ ~ is keyof Post

最後ですが、iskeyofを使用してcheckKeyをPostのプロパティのユニオン型として扱われるように変換してます
それぞれiskeyofを単体で見ていきます

isについて

const isPost = (obj: any): obj is Post => {
  return obj.textContent !== undefined && obj.createdAt !== undefined
}

const notPost = 'not post'

if(isPost(notPost)){
  console.log(notPost.id + 1)
}else{
  console.log('not Post type')
}

ユーザー定義した型かどうかを判定するために使えます

関数内で真偽値を返した時に、真であるなら引数として指定したプロパティを特定の型として扱うことができます
上記の例ではisPostを作成し、引数で受け取ったobjのプロパティとしてtextContent,createdAtが存在すれば
objはPost型として扱うという内容になります。

ただし、真であるならその型として扱われるため厳密な判定をしっかりしないと全然エラーになります。
(下記が一例)

const isPost = (obj: any): obj is Post => {
  return true
}

const c = 'not post'

if(isPost(notPost)){
  console.log(notPost.id + 1) // 存在しないプロパティにアクセスされ、実行時エラーとなる
}

keyofについて

interface Tweet{
  userId: number
  content: string
}

type KeyofTweetKeys = keyof Tweet
type UnionTweetKeys = 'userId' | 'content'

UnionTweetKeysKeyofTweetKeysは同じ内容を表します

型として定義したプロパティをユニオン型として返します

成功④解説

const keys = Object.keys(post)
const checkKey: string = 'id'

const checkPostType = (
  attribute: string,
  keys: string[]
): attribute is keyof Post => {
  return keys.includes(attribute)
}

if (checkPostType(checkKey, keys)) {
  console.log(post[checkKey])  // 成功④
}

先ほど説明したis,keyofを踏まえてみていきます。
今回は新しくcheckPostTypeを定義し、attribute isを設定しているので
真を返す場合attributeは、keyof Postとして扱います。

そのため、引数として受け取ったattributeがkeysに含まれている場合Postのプロパティのユニオン型として扱うため
条件分岐内でプロパティにアクセスできます

ジェネリクスを使用する例

interface Tweet {
  userId: number
  content: string
}

const checkType = <T extends string>(
  attribute: string,
  keys: string[]
): attribute is T => {
  return keys.includes(attribute)
}

const tweet: Tweet = {
  userId: 123,
  content: 'hoge',
}
const tweetKeys = Object.keys(tweet)
const checkTweetKey: string = 'id'

if (checkType<keyof Tweet>(checkTweetKey, tweetKeys)) {
  console.log(tweet[checkTweetKey])
}

自分はジェネリクスを使う機会が多くはないのですが
今回作成したものだと特定の型に限定されてしまいますが上手く型指定をして汎用的にした一例です

判定が真の時、第一引数に渡した値をジェネリクスとして指定したkeyof Tweetとして扱います

また、const checkType = <T extends string>としているのは
keyofとして受け取る値は文字列のユニオン型のためです

参考リンク

https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard#yznotype-guard
https://future-architect.github.io/typescript-guide/generics.html#id4

8
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?