1. ホーム
  2. TypeScript

【TypeScript】keyof と typeof(型演算子)の使い方|値やオブジェクトから型を取り出す

Share

TypeScript を書いていると、「あるオブジェクト型が持つキーだけを型として使いたい」「すでにある変数や定数から、いちいち手で書き直さずに型を取り出したい」という場面がよく出てきます。こうしたときに役立つのが、型演算子の keyoftypeof です。keyof はオブジェクト型からキーのユニオン型を作り、型コンテキストの typeof は値や変数から型を取り出します。この記事では、それぞれの基本から、両者を組み合わせて obj[key] を型安全に扱うジェネリック関数、そして as consttypeof で定数オブジェクトから型を作る実践例までを、初心者にも分かるように順を追って解説します。

keyof:オブジェクト型からキーのユニオン型を作る

keyof は、オブジェクトの型を渡すと、そのプロパティ名(キー)をすべて集めたユニオン型を返す型演算子です。ユニオン型とは 'a' | 'b' のように「このうちのどれか」を表す型のことです。たとえば次のように書くと、型 User が持つキーの一覧を型として取り出せます。

keyof-basic.ts
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 は、変数や値を渡すと、その値が持つ型を返す型演算子です。型を一から書き直さなくても、すでにある値から型を取り出せます。たとえば、初期値を持つオブジェクトから、それと同じ形の型を得たいときに便利です。

typeof-basic.ts
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型コンテキスト(型を書く場所)でだけ使うもので、実行時には何も起きず、コンパイル時に型を取り出すためのものです。

typeof-two-worlds.ts
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 を組み合わせる

keyoftypeof は、組み合わせて使うと真価を発揮します。まず typeof で値から型を取り出し、その型に keyof をかければ、値のプロパティ名のユニオン型を作れます。型を別に用意しなくても、値ひとつからキーの一覧が手に入ります。

keyof-typeof.ts
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 TT のキーを集めたユニオン型
typeof value(型の場所)value から推論される型
keyof typeof valuevalue のキーのユニオン型
T[K]T のキー K に対応するプロパティの型

型安全に obj[key] を扱うジェネリック関数

オブジェクトから指定したキーの値を取り出す関数を考えてみましょう。素朴に書くと戻り値の型が曖昧になりがちですが、keyofインデックスアクセス型T[K])を使うと、キーに応じた正確な型を返せます。T[K] は「型 T のキー K に対応するプロパティの型」を表す書き方です。

get-prop.ts
// 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 は「KT のキーのどれかでなければならない」という制約です。これにより、存在しないキーを渡すとコンパイルエラーになります。さらに戻り値を T[K] にしているため、'id' を渡せば number'name' を渡せば string、というようにキーごとに正しい型が返ります。any を使わずに、取り出す値の型まで守れるのがこの書き方の利点です。

as const と typeof で定数オブジェクトから型を作る

定数の一覧を型として使いたいときは、as consttypeof の組み合わせが定番です。as const は、値を読み取り専用かつリテラル型として固定する指定です。これを付けると、typeof で取り出したときに具体的な値までが型に反映されます。

as-const.ts
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 と組み合わせると、定数オブジェクトからキーと値の型を取り出せます。型と値の二重管理を減らし、定義の変更に強いコードを書くための基本テクニックとして、ぜひ使いこなしてください。

参考ページ