🦁

特定の型のキーだけ抽出する方法

2022/05/04に公開

始めに

Objectの型から例えばvalueがstringになるキーだけを抽出したいときの方法です。

type Person = {
  id: number
  name: string
  height: number
  job: string
}

// stringだけを持つキーを抽出したい
type PersonLabelKey = 'name' | 'job'

結論

結論は以下のようなユーティリティを用意することで実現できます。

type OnlyTypeKey<T extends Object, ExpectType> = {
  [K in keyof T]: T[K] extends ExpectType ? K : never
}[keyof T]

type PersonLabelKey = OnlyTypeKey<Person, string>
/*
// 最初にvalueにkey名のリテラルかneverに変換される
{
  id: never,
  name: 'name',
  height: never,
  job: 'job'
}

// ここからkeyof Tでvalueリストを見て、keyリストが出る
'name' | 'job'
*/

使用例

そもそもなんでこんなことをやったかというと、リストから該当のアイテムをfindして適切なラベルを表示するという処理をしょっちゅう書いていて共通化できないかなぁと思ったのがキッカケです。labelKeyというラベルになるkeyを限定的にすることでサジェスト時のkeyの数が絞られますし、型安全にもなります。

function findItemLabel<T extends Object>(
  items: readonly T[],
  searchItem: Partial<T>,
  labelKey: OnlyTypeKey<T, string>
) {
  const searchItemKeys = Object.keys(searchItem) as Array<keyof T>
  const item = items.find((item) => searchItemKeys.every((key) => item[key] === searchItem[key]))
  if (item == null) {
    return ''
  }
  return item[labelKey]
}

const PERSONS: Array<Person> = [
  { id: 0, name: '山田太郎', height: 170, job: 'エンジニア' },
  { id: 1, name: '山田花子', height: 160, job: 'デザイナー' },
  { id: 2, name: '佐藤太郎', height: 175, job: 'エンジニア' },
]

// 第2引数で検索オブジェクトを渡し、その結果のラベルを第3引数で指定してしまう
const personName = findItemLabel(PERSONS, { id: 0 }, 'name')

終わりに

以上が特定の型のキーだけ抽出する方法でした。あまり使う機会はないと思いますが、string以外にもなり得るkeyが混ざっていると時にはtypeエラーになる場合があるのでその時にでもご参考にしていただけると幸いです。
今回試したサンプルコードはこちらで見れますので、興味がある方は見てください。

https://www.typescriptlang.org/play?#code/C4TwDgpgBA8gdgGxAFXBA0hEAeZUIAewEcAJgM6wBGAVhAMbAA0UAogZI6pAHxQC8UAN4AoKFADa6KAEs4UANZYA9gDMoyALoAuDVM34iJCmw4Ng3aAH4o03XAgA3CACcRAXwlKQajZpEiqgCucIwyyvKqcqQAksQAtgAyAIZUEAi4hsRklDC05jwAFGKyCeS6LhDJpBFIeppMJeRVLvQAFnEQ8boACskuwDLJGcg8jeIIqemYILrwSJYzuCzkwC5yAOY8IgCUwiX0EatQzf3tnfEzlIJ5dIwAdN7khaetHQl7yZQAgi4uyThvL5RgcjsBSl0BBD4uR7lEyIVCjIPgI+K9zgkrvcnK4QIjvHt+HxkV0vFgDPxKScWhjSd5NDsdiUZOokQkBII4EEEAg9qJxOJKsAgi55AByMUldwlIUi+Qk+ISSZpBAzfzSkSgSBQHqucgRKH82SkexBeJpNziODJeIQXSrdZwDYlNoQGQbNrAU3m1wlGjKKj2tabDwBQ5wY49VgAJQAyjAAHKx3S-f44XUufVwPiCCQlITG3QABhY1ttujFgEcdQAMroAqTUAcwliliu92e3QARgA7CWoP7A1AxYAKhkAzwyADoZANMMgCKGMVQdzjYSFqDt0s2u0DmuARqDAAraTagLY9XqXADYe32K4BxhkAbQyAEoZAFMMgB+GGdz-OLgBMK-LA8ACvKAE9CG7v9zbJdOwAVhYM8BxHCdp1nRp-BEcNjkgTMIgTVcoXhWIEhSFVCijONE1jFgCxkE0oCLWCBzLCAxSZRDlAQCB7gQZQNkKZCszQ20mSAA

Discussion