TypeScript を書いていると、「あるオブジェクト型が持つキーだけを型として使いたい」「すでにある変数や定数から、いちいち手で書き直さずに型を取り出したい」という場面がよく出てきます。こうしたときに役立つのが、型演算子の keyof と typeof です。keyof はオブジェクト型からキーのユニオン型を作り、型コンテキストの typeof は値や変数から型を取り出します。この記事では、それぞれの基本から、両者を組み合わせて obj[key] を型安全に扱うジェネリック関数、そして as const と typeof で定数オブジェクトから型を作る実践例までを、初心者にも分かるように順を追って解説します。
目次
keyof:オブジェクト型からキーのユニオン型を作る
keyof は、オブジェクトの型を渡すと、そのプロパティ名(キー)をすべて集めたユニオン型を返す型演算子です。ユニオン型とは 'a' | 'b' のように「このうちのどれか」を表す型のことです。たとえば次のように書くと、型 User が持つキーの一覧を型として取り出せます。
type User = {
id: number;
name: string;
age: number;
};
// User のキーをすべて集めたユニオン型
type UserKey = keyof User;
// UserKey は 'id' | 'name' | 'age' と同じ意味になる
const key1: UserKey = 'name'; // OK
// const key2: UserKey = 'email'; // エラー: 'email' は User のキーにない
keyof User は 'id' | 'name' | 'age' という文字列リテラルのユニオン型になります。ポイントは、User の定義を変えれば UserKey も自動的に追いかけて変わることです。キーの一覧を別の場所に手書きで持つ必要がなくなり、型の食い違いを防げます。なお、キーが数値の場合は number、シンボルの場合は symbol を含むこともあります。
型コンテキストの typeof:値・変数から型を取り出す
typeof は、変数や値を渡すと、その値が持つ型を返す型演算子です。型を一から書き直さなくても、すでにある値から型を取り出せます。たとえば、初期値を持つオブジェクトから、それと同じ形の型を得たいときに便利です。
const defaultUser = {
id: 0,
name: 'ゲスト',
age: 20,
};
// 値 defaultUser から型を取り出す
type DefaultUser = typeof defaultUser;
// DefaultUser は { id: number; name: string; age: number } と推論される
// 取り出した型を別の変数に使い回せる
const another: DefaultUser = {
id: 1,
name: '田中',
age: 30,
};
typeof defaultUser は、TypeScript が推論した defaultUser の型(この場合 { id: number; name: string; age: number })をそのまま型として使えるようにします。値の定義がひとつあれば、それに合わせた型が自動で手に入るので、型と値の二重管理を避けられます。
JavaScript の実行時 typeof とは別物
ここは初心者がとても混乱しやすいところなので、はっきり区別しておきましょう。同じ typeof というキーワードでも、使う場所によって意味がまったく違います。JavaScript の typeof は実行時に動き、値の種類を表す文字列('string'・'number'・'object' など)を返します。一方、この記事で扱っている typeof は型コンテキスト(型を書く場所)でだけ使うもので、実行時には何も起きず、コンパイル時に型を取り出すためのものです。
const value = 'hello';
// (1) 実行時の typeof(JavaScript): 文字列 'string' を返す
const kind = typeof value; // kind の中身は "string"
console.log(kind); // "string" と表示される
// (2) 型コンテキストの typeof(TypeScript): 型を取り出す
type ValueType = typeof value; // ValueType は string 型
// 見分け方: type や : のあとなど「型を書く場所」にある typeof は (2)、
// 値の式として書かれている typeof は (1)
見分ける目安は「どこに書かれているか」です。type ○○ = の右側や、変数の型注釈(: のあと)など型を書く場所にある typeof は型を取り出す方(TypeScript)です。const x = typeof ... のように値の式として書かれている typeof は文字列を返す方(JavaScript)です。この記事で扱うのは、あくまで型を書く場所で使う typeof です。
keyof と typeof を組み合わせる
keyof と typeof は、組み合わせて使うと真価を発揮します。まず typeof で値から型を取り出し、その型に keyof をかければ、値のプロパティ名のユニオン型を作れます。型を別に用意しなくても、値ひとつからキーの一覧が手に入ります。
const config = {
host: 'localhost',
port: 8080,
secure: false,
};
// typeof config で値から型を取り出し、その keyof でキーの一覧を得る
type ConfigKey = keyof typeof config;
// ConfigKey は 'host' | 'port' | 'secure'
function getConfig(key: ConfigKey) {
return config[key];
}
getConfig('host'); // OK
// getConfig('user'); // エラー: 'user' は config のキーにない
keyof typeof config は「config という値が持つキーのユニオン型」です。演算の順番は、まず typeof config で型を取り出し、そのあと keyof でキーを集める、という流れになります。よく使う組み合わせなので、この形はそのまま覚えておくと便利です。次の表に、ここまでに出てきた記法を整理します。
| 記法 | 意味 |
|---|---|
keyof T | 型 T のキーを集めたユニオン型 |
typeof value(型の場所) | 値 value から推論される型 |
keyof typeof value | 値 value のキーのユニオン型 |
T[K] | 型 T のキー K に対応するプロパティの型 |
型安全に obj[key] を扱うジェネリック関数
オブジェクトから指定したキーの値を取り出す関数を考えてみましょう。素朴に書くと戻り値の型が曖昧になりがちですが、keyof とインデックスアクセス型(T[K])を使うと、キーに応じた正確な型を返せます。T[K] は「型 T のキー K に対応するプロパティの型」を表す書き方です。
// K は T のキーのどれかに限定される
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = {
id: 1,
name: '佐藤',
active: true,
};
const id = getProp(user, 'id'); // id は number 型
const name = getProp(user, 'name'); // name は string 型
const active = getProp(user, 'active'); // active は boolean 型
// getProp(user, 'email'); // エラー: 'email' は user のキーにない
K extends keyof T は「K は T のキーのどれかでなければならない」という制約です。これにより、存在しないキーを渡すとコンパイルエラーになります。さらに戻り値を T[K] にしているため、'id' を渡せば number、'name' を渡せば string、というようにキーごとに正しい型が返ります。any を使わずに、取り出す値の型まで守れるのがこの書き方の利点です。
as const と typeof で定数オブジェクトから型を作る
定数の一覧を型として使いたいときは、as const と typeof の組み合わせが定番です。as const は、値を読み取り専用かつリテラル型として固定する指定です。これを付けると、typeof で取り出したときに具体的な値までが型に反映されます。
const ROLES = {
admin: 'ADMIN',
editor: 'EDITOR',
viewer: 'VIEWER',
} as const;
// キーのユニオン型: 'admin' | 'editor' | 'viewer'
type RoleKey = keyof typeof ROLES;
// 値のユニオン型: 'ADMIN' | 'EDITOR' | 'VIEWER'
type RoleValue = (typeof ROLES)[RoleKey];
function setRole(role: RoleValue) {
console.log(`ロールを ${role} に設定`);
}
setRole('ADMIN'); // OK
// setRole('GUEST'); // エラー: 'GUEST' は RoleValue にない
as const を付けたことで、ROLES の各値は string ではなく 'ADMIN' のようなリテラル型になります。そのため (typeof ROLES)[RoleKey] は、値をすべて集めた 'ADMIN' | 'EDITOR' | 'VIEWER' というユニオン型になります。(typeof ROLES)[RoleKey] は「ROLES の型の、いずれかのキーに対応する値の型」という意味です。もし as const を付けなければ、値は string と推論され、この絞り込みは効かなくなります。定数の一覧と、それを使う型を一箇所で管理できるのが、この書き方の大きな利点です。
まとめ
keyof はオブジェクト型からキーのユニオン型を作り、型コンテキストの typeof は値や変数から型を取り出す演算子です。この typeof は、実行時に文字列を返す JavaScript の typeof とは別物で、型を書く場所でのみ使う点に注意してください。両者を keyof typeof のように組み合わせれば、値からキーの一覧を得られ、T[K] と合わせれば obj[key] を型安全に扱うジェネリック関数も書けます。さらに as const と組み合わせると、定数オブジェクトからキーと値の型を取り出せます。型と値の二重管理を減らし、定義の変更に強いコードを書くための基本テクニックとして、ぜひ使いこなしてください。