読み込み中...

TypeScriptのkeyofを完全解説!たった13の手順で完全マスター

TypeScriptのkeyofを使ったコード例とその詳細解説のイメージ TypeScript
この記事は約21分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

TypeScriptはJavaScriptのスーパーセットとして、強力な型システムを持ち、その中で「keyof」というキーワードが非常に有用な機能として提供されています。

この記事では、TypeScript初心者でもkeyofの基本から応用、そして実践的なサンプルコードまでを、徹底的に解説していきます。

●keyofとは

TypeScriptにおいて、オブジェクトのプロパティ名を取得する際に使用する「keyof」は、オブジェクトのキーを文字列リテラルのユニオン型として取得できる非常に便利なキーワードです。

例えば、オブジェクトのプロパティ名を動的に扱いたい場合や、特定のオブジェクトのキーだけを許容したい場合などに使います。

○TypeScriptでの役割

TypeScriptは、静的型付けを提供しており、これによりコンパイル時に型のエラーを検出することができます。

この型安全性を保つための1つのキーワードが「keyof」です。

keyofを使うことで、オブジェクトのキーを取得し、それを利用してさまざまな型操作を行うことができます。

これにより、実行時ではなく、コンパイル時にエラーをキャッチすることができ、より安全なコードを書くことが可能となります。

○基本的な文法

keyofの基本的な文法を説明する前に、まず簡単なオブジェクトを用意します。

type Person = {
    name: string;
    age: number;
};

このコードでは、Personという型を定義しており、nameプロパティとageプロパティを持っています。

keyofを使用して、Personのキーを取得する方法は次の通りです。

type PersonKeys = keyof Person;

このコードでは、PersonKeysという新しい型を定義しています。

この型は、Personのすべてのキーを文字列リテラルのユニオン型として持っています。

つまり、PersonKeysの型は”name” | “age”となります。

このコードを実行すると、PersonKeysには”name”と”age”という2つのキーが含まれていることがわかります。

●keyofの使い方

TypeScriptは、JavaScriptのスーパーセットとして開発された静的型付け言語です。

そして、その中でも「keyof」というキーワードは非常に強力なツールとして知られています。

ここでは、TypeScriptでの「keyof」の役割や使い方、そして基本的な文法を徹底的に解説していきます。

実践的なサンプルコードとともに、初心者の方でも理解しやすくなるように解説します。

○サンプルコード1:オブジェクトのプロパティを取得

JavaScriptでは、オブジェクトのプロパティ名を取得する際に「Object.keys」を使用しますが、TypeScriptでは「keyof」を使用して、オブジェクトのプロパティ名を型として取得できます。

その具体的なサンプルコードを紹介します。

type Person = {
    name: string;
    age: number;
    address: string;
};

type PersonKeys = keyof Person;  // "name" | "age" | "address"

このコードでは、Personという型を定義しています。

そして、keyofを使って、Personのプロパティ名を型として取得して、新たな型PersonKeysを作成しています。

その結果、PersonKeysは”name” | “age” | “address”というユニオン型を持つことになります。

このように、keyofを使用することで、オブジェクトのプロパティ名を型として簡単に取得することができます。

また、この機能を利用することで、動的に変化するオブジェクトのプロパティに対して、安全にアクセスすることができます。

例として、次の関数を考えてみましょう。

function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const person: Person = {
    name: "太郎",
    age: 25,
    address: "東京都"
};

const personName = getValue(person, "name");  // 太郎

このコードを実行すると、getValue関数は指定されたオブジェクトの指定されたキーの値を返します。

この場合、personオブジェクトの”name”プロパティの値である”太郎”が返されます。

○サンプルコード2:型としての使用方法

TypeScriptの強力な型システムを活かして、オブジェクトのプロパティ名を型として活用する方法を学びます。

これは、特に大規模なアプリケーションやライブラリを開発する際に、型の安全性を保ちつつ柔軟にコードを書くための技術として役立ちます。

interface Person {
    name: string;
    age: number;
    address: string;
}

// keyofを使って、Personオブジェクトのプロパティ名を型として取得
type PersonKeys = keyof Person;

このコードでは、Personという名前のinterfaceを定義しています。

その後にkeyofを使って、Personのプロパティ名を取得し、それを新たな型PersonKeysとして定義しています。

このようにして取得した型は、文字列リテラルのユニオン型として解釈されます。

すなわち、PersonKeysの型は"name" | "age" | "address"となります。

この特性を使うことで、次のような関数を定義することができます。

function getPersonValue(person: Person, key: PersonKeys): string | number {
    return person[key];
}

このコードを実行すると、getPersonValue関数は、Personオブジェクトと、そのプロパティ名を表す文字列を引数として受け取り、対応するプロパティの値を返す機能を持ちます。

keyofを使用することで、引数keyとして指定できるプロパティ名をPersonインターフェースに定義されているものに限定しています。

これにより、存在しないプロパティ名を誤って指定することがなくなり、型の安全性が向上します。

例えば、次のようなコードを考えます。

const tom: Person = {
    name: "Tom",
    age: 25,
    address: "Tokyo"
};

const nameValue = getPersonValue(tom, "name");

上記のコードを実行すると、変数nameValueにはTomという文字列が格納されます。

また、getPersonValue(tom, "hobby")のように、Personインターフェースに存在しないプロパティ名を指定しようとすると、コンパイル時にエラーとして検出されます。

○サンプルコード3:関数の引数に適用

TypeScriptの強力な型システムを活用する際に、関数の引数としてkeyofを活用する方法があります。

これにより、特定のオブジェクトのプロパティ名のみを引数として受け取る関数を作成することが可能となります。

ここでは、その使用方法と具体的なサンプルコードを通して、関数の引数にkeyofを適用する方法を詳しく見ていきましょう。

まず、次のサンプルコードは、Person型のオブジェクトを持ち、指定されたプロパティ名を引数として受け取り、そのプロパティの値を返す関数getPropertyを表しています。

type Person = {
  name: string;
  age: number;
  address: string;
};

function getProperty(obj: Person, key: keyof Person): Person[keyof Person] {
  // keyがPersonのプロパティ名であることを保証
  return obj[key];
}

// 使用例
const taro: Person = {
  name: "Taro",
  age: 30,
  address: "Tokyo"
};

const result = getProperty(taro, "name"); // "Taro"という値が返されます

このコードでは、keyofを使って、getProperty関数の第二引数としてPerson型のプロパティ名のみを受け取るように指定しています。

このため、誤って存在しないプロパティ名を指定すると、コンパイル時にエラーが発生します。

実際に上記のコードを実行すると、resultにはtaroオブジェクトのnameプロパティの値である”Taro”が格納されます。

このように、keyofを関数の引数に適用することで、オブジェクトの特定のプロパティの値を安全に取得することが可能になります。

次に、関数の戻り値にkeyofを使ったサンプルコードを見てみましょう。

function getKeys<T>(obj: T): Array<keyof T> {
  return Object.keys(obj) as Array<keyof T>;
}

const keys = getKeys(taro); // ["name", "age", "address"] という配列が返されます

上記のコードでは、ジェネリックを活用して、任意のオブジェクトを引数にとり、そのオブジェクトのすべてのプロパティ名を配列として返す関数getKeysを定義しています。

実行すると、keysには[“name”, “age”, “address”]という配列が格納されます。

●keyofの応用例

TypeScriptを使う中で、基本的な使い方だけでなく、keyofを応用した方法も知っておくことが大切です。

ここでは、keyofを更に深く理解するための応用的なサンプルコードをいくつか取り上げ、詳細に解説します。

○サンプルコード4:マッピング型での使用

マッピング型とは、既存の型から新しい型を作るためのTypeScriptの高度な機能の一つです。

keyofと組み合わせることで、非常に柔軟な型を作成することができます。

keyofとマッピング型を組み合わせたサンプルコードを紹介します。

type User = {
    name: string;
    age: number;
};

type UserToBoolean = {
    [K in keyof User]: boolean;
};

このコードでは、User型が定義されており、nameとageという2つのプロパティを持っています。

そして、UserToBooleanという新しい型をマッピング型を用いて作成しています。

[K in keyof User]: boolean という部分がマッピング型のキーです。

これは「User型のすべてのキーに対して、それぞれのプロパティがboolean型である」という新しい型を作成しています。

この結果、UserToBoolean型は { name: boolean; age: boolean; } という形になります。

もし、User型に新しいプロパティが追加された場合、UserToBoolean型も自動的に更新されるのがポイントです。

このマッピング型の特徴は、元の型の変更に強く、型の整合性を保ちやすいという点にあります。

特に大規模なプロジェクトでの使用において、型の管理が容易になります。

このコードを実行すると、特に出力はされませんが、UserToBooleanという型が定義されることになります。

この型は、User型の各プロパティをboolean型に変換したものです。

続いて、この新しい型を利用して実際のオブジェクトを作成してみましょう。

const userFlags: UserToBoolean = {
    name: true,
    age: false
};

userFlagsというオブジェクトは、UserToBoolean型に基づいており、各プロパティはboolean型の値を持っています。

これにより、User型の各プロパティに関連するフラグを管理することができます。

この応用技法は、大規模なプロジェクトや複雑な型の管理が求められる場面で非常に役立ちます。

keyofとマッピング型の組み合わせにより、効率的な型定義が可能になるのです。

○サンプルコード5:条件付き型と組み合わせ

TypeScriptでは、型の扱いをより柔軟に行うための機能として、条件付き型が提供されています。

条件付き型は、ある型が特定の条件を満たすかどうかに基づいて型を決定する強力な機能です。

そして、keyofと組み合わせることで、より高度な型制御を実現することが可能になります。

具体的なコードを見ながら、どのようにkeyofと条件付き型を組み合わせるかを紹介します。

// オブジェクトの型定義
type Person = {
  name: string;
  age: number;
  isStudent: boolean;
};

// 条件付き型を用いた型抽出
type StringKeys<T> = {
  [K in keyof T]: T[K] extends string ? K : never;
}[keyof T];

上記のコードでは、Personという型を定義しています。

そして、StringKeysという条件付き型を定義しています。

この型は、オブジェクトのキーを探索し、そのキーが指す値の型が文字列である場合に、そのキーを型として抽出するものです。

このコードを実行すると、文字列を値とするプロパティのキーのみを抽出することができます。

実際にStringKeys型をPerson型に適用してみます。

type Result = StringKeys<Person>;
// Result型は 'name' となります。

Result型はnameのみとなります。

なぜなら、Person型の中で文字列型を持つプロパティはnameのみだからです。

○サンプルコード6:ジェネリックスでの活用

TypeScriptのジェネリックスは、型の再利用性を高めるための強力な機能です。

keyofとジェネリックスを組み合わせることで、より動的かつ柔軟な型定義が可能となります。

ここでは、ジェネリックスを活用してkeyofを使用する具体的なサンプルコードを解説します。

// Tというジェネリック型を使用して関数を定義
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];  // T[K] は、オブジェクトTのプロパティKの型を意味します。
}

// サンプルオブジェクト
const sampleObj = {
    name: 'Taro',
    age: 25
};

// 上記の関数を使用して、プロパティを取得
const userName = getProperty(sampleObj, 'name'); // 'Taro'
const userAge = getProperty(sampleObj, 'age');   // 25

このコードでは、ジェネリックスを使ってgetPropertyという関数を定義しています。

この関数は2つの型引数、TとKを取ります。Kはkeyof Tという形で定義されており、これによってKはTのプロパティの名前の型となります。

このようにして、オブジェクトのプロパティを動的に取得する関数を型安全に実装できます。

このコードを実行すると、userNameには’sampleObj’の’name’プロパティの値である’Taro’が、userAgeには’age’プロパティの値である25がそれぞれ格納されます。

このように、ジェネリックスを利用することで、さまざまなオブジェクトとプロパティで再利用可能な関数を作成することができます。

さらに、ジェネリックスを使用することで、誤って存在しないプロパティ名を指定すると、コンパイル時にエラーが発生します。

例えば、getProperty(sampleObj, 'address')のように、’sampleObj’に存在しない’address’プロパティを取得しようとすると、TypeScriptの型チェッカーはエラーを報告します。

○サンプルコード7:組み込みユーティリティ型との組み合わせ

TypeScriptには、あらかじめ定義されたユーティリティ型というものが多数存在しています。

これらのユーティリティ型は、日常の開発作業を大いに助けてくれるものとなっており、keyofと組み合わせることで、さらなる強力な型の制御が可能になります。

例として、よく使用されるPartialというユーティリティ型を挙げます。

このPartial型は、与えられた型のすべてのプロパティをオプションに変換する役割を持っています。

下記のサンプルコードでは、Personという型を定義し、その後Partialkeyofを使用して、Person型のプロパティをすべてオプショナルにした新しい型を生成します。

// Person型の定義
type Person = {
    name: string;
    age: number;
    address: string;
};

// keyofを使用して、Person型のプロパティ名を取得する
type PersonKeys = keyof Person;

// Partialとkeyofを組み合わせて、Person型のすべてのプロパティをオプショナルにした新しい型を生成
type PartialPerson = {
    [K in PersonKeys]?: Person[K];
};

// 実際にPartialPerson型を使用する
const person: PartialPerson = {
    name: "Taro"
};

このコードでは、Person型の3つのプロパティnameageaddressのいずれか、あるいはそのすべてを持つオブジェクトを作成することができます。

このように、Partialkeyofを組み合わせることで、非常に柔軟な型制御が可能になります。

このコードを実行すると、personオブジェクトにはnameプロパティのみが存在する状態となります。

しかし、ageaddressのプロパティも追加することができるようになっています。

このような型は、APIのレスポンスやフォームの入力値など、部分的なデータを扱う際に非常に役立ちます。

●注意点と対処法

TypeScriptを使いこなす上で、特にkeyofを利用する際に気をつけるべき点や、一般的なトラブルを解決するためのテクニックを取り上げます。

これから述べる注意点や解決策を身につけることで、TypeScriptのコーディングがさらにスムーズになります。

○keyofの落とし穴

TypeScriptのkeyofは非常に強力なツールですが、その性質上、使用する際にはいくつかの落とし穴が存在します。

□具体的な型ではなく、文字列リテラル型の集合として返される

keyofを使うと、オブジェクトのプロパティ名を文字列リテラル型の集合として取得することができます。

しかし、その結果は具体的な値の型ではありません。

これは、特定の処理を期待してkeyofを使用すると、予期せぬ結果を引き起こすことがあります。

このコードではPersonオブジェクトのプロパティ名を取得しています。

type Person = {
    name: string;
    age: number;
};

type PersonKeys = keyof Person; // 'name' | 'age'

コメントにあるように、PersonKeysの型は’name’ | ‘age’となります。

これは文字列リテラル型の集合です。

□keyofとunion型の組み合わせ

union型とkeyofを組み合わせると、すべてのunionのメンバーのキーのunionが生成されることがあります。

type A = {
    x: number;
};

type B = {
    y: string;
};

type Keys = keyof (A | B);  // "x" | "y"

上記のコードでは、AとBのunion型のキーを取得しています。

Keysの型は”x” | “y”となります。

○予期せぬ型エラーへの対処

TypeScriptのkeyofを使用していると、型エラーが発生することがあります。

それらの典型的なエラーとその対処法を紹介します。

□オブジェクトに存在しないプロパティへのアクセス

keyofを使用して、オブジェクトのプロパティに動的にアクセスしようとする場合、存在しないプロパティにアクセスしてしまうことがあります。

これは、keyofが返す型が狭すぎる、または広すぎる場合に発生することがあります。

type Person = {
    name: string;
    age: number;
};

function getProperty(obj: Person, key: keyof Person) {
    return obj[key];
}

const person: Person = {
    name: "Taro",
    age: 25
};

const value = getProperty(person, "address"); // エラー!addressはPersonのプロパティではありません。

このコードを実行すると、”address”はPersonのプロパティではないため、エラーが発生します。

このような問題を避けるためには、関数の引数で受け取るキーがオブジェクトのプロパティであることを確認する必要があります。

□型アサーションを使用する

型が予期せぬエラーを引き起こす場合、型アサーションを使用して型を上書きすることができます。

ただし、この方法は注意が必要です。

型アサーションはコンパイラの型チェックをオーバーライドするため、間違った型をアサーションすると、ランタイムエラーの原因となる可能性があります。

type Person = {
    name: string;
    age: number;
};

function getProperty(obj: any, key: any): any {
    return obj[key as keyof typeof obj];
}

const person: Person = {
    name: "Taro",
    age: 25
};

const value = getProperty(person, "address");  // これはエラーを引き起こさないが、addressプロパティは存在しないためundefinedが返される。

このコードでは、getProperty関数内で型アサーションを使用しています。

この結果、addressという存在しないプロパティにアクセスしてもエラーは発生しませんが、返される値はundefinedになります。

●カスタマイズ方法

TypeScriptのkeyofをさらにパワフルに使いこなすためのカスタマイズ方法について解説します。

TypeScriptは柔軟な言語であり、その機能をカスタマイズして、さらに具体的なニーズに合わせて使用することができます。

それでは、keyofを中心としたカスタマイズ方法をいくつかの実践的なサンプルコードとともにご紹介します。

○keyofの拡張とカスタマイズのアイディア

□絞り込みを使用してkeyofの型をさらに限定する

keyofを使用してオブジェクトのプロパティ名を取得する際、特定の型のプロパティだけを対象にすることができます。

このコードでは、Person型の中でstring型のプロパティ名だけを取得しています。

type Person = {
    name: string;
    age: number;
    country: string;
};

type StringKeys<T> = {
    [K in keyof T]: T[K] extends string ? K : never;
}[keyof T];

type PersonStringKeys = StringKeys<Person>; // 'name' | 'country'

このコードを実行すると、PersonStringKeysの型は’name’ | ‘country’となります。

ageプロパティはnumber型のため、結果から除外されています。

□keyofと組み合わせたマッピング型のカスタマイズ

keyofを使用してマッピング型を作成する際、カスタマイズを施すことで、さらに詳細な型のマッピングを行うことができます。

下記のコードは、オブジェクトのプロパティをreadonlyに変換し、さらにそれぞれのプロパティの末尾に”_readonly”を追加する例です。

type Person = {
    name: string;
    age: number;
};

type ReadonlyWithSuffix<T> = {
    [K in keyof T as `${K}_readonly`]: Readonly<T[K]>
};

type ModifiedPerson = ReadonlyWithSuffix<Person>; 
// { name_readonly: string; age_readonly: number }

このコードを実行すると、ModifiedPersonの型は{name_readonly: string; age_readonly: number}となります。

まとめ

この記事では、TypeScriptのkeyofに関する全体的な理解を目指し、その基本から応用、そしてカスタマイズ方法までを13のステップで徹底的に解説しました。

keyofは、オブジェクトのプロパティを型として取得する際の強力なツールとなります。

正確に使えば、TypeScriptでの型の取り扱いをより効果的に行うことが可能となります。

TypeScript初心者から経験者まで、keyofをより効果的に使うためのヒントや技術を得ることができたことを確信しています。

最後まで読んでいただき、誠にありがとうございました。

今後もTypeScriptの魅力を最大限に活かすための知識や技術を学び続けて、より高品質なコードを書くことを目指してください。