keyof は「プロパティ名だけを取り出すための型演算子」
keyof は、「オブジェクト型から“プロパティ名”だけを取り出して、新しい型を作る」ための演算子です。
まずは一番シンプルな例から見てみましょう。
type Person = {
name: string;
age: number;
};
type PersonKey = keyof Person;
TypeScriptこのとき PersonKey は、次のような型になります。
type PersonKey = "name" | "age";
TypeScriptつまり、keyof Person は「Person 型が持っているプロパティ名を、文字列リテラルのユニオン型として取り出したもの」です。
ここで大事なのは、「値」ではなく「型としてのプロパティ名」を扱っているという感覚です。
基本の動き:プロパティが増えると keyof の結果も増える
複数プロパティを持つ型に対する keyof
プロパティが増えると、その分だけ keyof の結果も増えます。
type Book = {
title: string;
price: number;
rating: number;
};
type BookKey = keyof Book;
TypeScriptこのとき BookKey は、次のようになります。
type BookKey = "title" | "price" | "rating";
TypeScriptBook にプロパティを追加すれば、keyof Book の結果も自動的に増えます。
「プロパティ名の一覧を、型として自動で持っておける」のが keyof の一番の価値です。
もしこれを手書きでやると、
type BookKey = "title" | "price" | "rating";
TypeScriptのように、プロパティを追加するたびにここも直さないといけません。keyof を使えば、「型定義を1か所直すだけで、プロパティ名のユニオン型も勝手に追従してくれる」状態になります。
keyof を使うと何がうれしいのか(イメージを掴む)
「プロパティ名を引数に取りたい」関数で真価を発揮する
たとえば、オブジェクトから特定のプロパティだけを取り出す関数を考えます。
type User = {
id: number;
name: string;
age: number;
};
function getProp(obj: User, key: "id" | "name" | "age") {
return obj[key];
}
TypeScriptこれでも動きますが、"id" | "name" | "age" を手で書いているのがダルいし、
あとから User にプロパティを追加したときに、ここを直し忘れるリスクがあります。
ここで keyof の出番です。
function getProp(obj: User, key: keyof User) {
return obj[key];
}
TypeScriptkeyof User は "id" | "name" | "age" と同じ意味なので、
「User に存在するプロパティ名しか渡せない関数」になります。
const u: User = { id: 1, name: "Taro", age: 20 };
getProp(u, "id"); // OK
getProp(u, "name"); // OK
getProp(u, "age"); // OK
// getProp(u, "email"); // エラー: '"email"' は 'keyof User' に含まれない
TypeScriptここで守られているのは、
- 存在しないプロパティ名(スペルミスなど)を渡せない
- 型定義を変えたときに、関数側を手で直さなくていい
という2点です。
「プロパティ名を“文字列”ではなく“型”として扱う」ことで、安全性とメンテナンス性が一気に上がるのが keyof の本質です。
keyof とインデックスシグネチャの関係(少しだけ)
インデックスシグネチャを持つ型に対する keyof
keyof は、インデックスシグネチャ([key: string]: ... のような書き方)にも使えます。
type MapLike = {
[key: string]: any;
};
type MapKeys = keyof MapLike;
TypeScriptこのとき MapKeys は、string | number になります。
「なんで string だけじゃなくて number も入るの?」と思うかもしれませんが、
JavaScript ではオブジェクトのキーは内部的にすべて文字列に変換されるので、obj[0] と obj["0"] は同じ扱いになる、という仕様が背景にあります。
初心者のうちは、
「インデックスシグネチャに対する keyof は、“そのキーの型”が返ってくる」
くらいの理解で十分です。
keyof と typeof を組み合わせると「値から型」を作れる
実際のオブジェクトから「キーの型」を取り出す
keyof は「型」に対して使う演算子ですが、typeof と組み合わせると「値から型を作って、そのキーを取り出す」ということができます。
const user = {
id: 1,
name: "Taro",
age: 20,
};
type User = typeof user;
type UserKey = keyof User;
TypeScriptこのとき UserKey は、"id" | "name" | "age" になります。
つまり、
typeof userで「userの型」を取り出しkeyofで「その型のプロパティ名」を取り出している
という流れです。
これを使うと、「実際のオブジェクトの構造から、そのキーのユニオン型を自動で作る」ことができます。
「値 → 型 → キーの型」というルートを辿れるようになると、型の表現力が一気に広がります。
初心者がまず掴んでおきたい keyof の感覚
最後に、keyof をどう捉えるとスッと入ってくるかをまとめます。
keyof は、
「このオブジェクト型が持っているプロパティ名を、型として欲しい」
と思ったときに使う道具です。
その結果として、
- プロパティ名のユニオン型を自動で作れる
- 関数の引数として「存在するプロパティ名だけ」を受け取れる
- 型定義を変えたときに、プロパティ名の列挙を手で直さなくてよくなる
というメリットが生まれます。
まずはこういう小さなところから使ってみるのがおすすめです。
type User = { id: number; name: string };
type UserKey = keyof User; // "id" | "name"
function hasKey(obj: User, key: UserKey) {
return key in obj;
}
TypeScript「プロパティ名を“文字列”ではなく“型”として扱う」
この感覚に慣れてくると、keyof はただの記号ではなく、
「オブジェクトの構造と操作を結びつけるための、かなり強力な型の道具」として見えてきます。
