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
最後ですが、is
とkeyof
を使用してcheckKeyをPostのプロパティのユニオン型として扱われるように変換してます
それぞれis
とkeyof
を単体で見ていきます
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'
UnionTweetKeys
とKeyofTweetKeys
は同じ内容を表します
型として定義したプロパティをユニオン型として返します
成功④解説
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