【TypeScript】keyof・typeofとは?「使い方」や「違い」をわかりやすく解説!

TypeScriptの型システムを理解するうえで、避けて通れないのがkeyoftypeofといった演算子です。

でも、初めて見ると…

  • これって何をしてるの?
  • どういう場面で使うの?
  • JavaScriptのtypeofとは何が違うの?

といった疑問が浮かんでくるかもしれません。

この記事では、TypeScriptでよく使われるkeyoftypeofについて、以下の内容をサンプルコードを用いてわかりやすく解説します。

keyofとは

keyof「オブジェクト型のプロパティ名(キー名)を列挙したユニオン型」を返す演算子です。keyofは「型」に対して使用するもので、「変数」や「定数」には使えないので注意しましょう。

以下にサンプルコードを示します。

type Person = {
  id: number;
  name: string;
  email: string;
};

type PersonKeys = keyof Person;
// 上は次と同じ意味になる
type PersonKeys = 'id' | 'name' | 'email';

PersonKeysという型は「文字列"id""name""email"」のどれかしか許されない型になります。つまり、Person型のプロパティ名だけを集めたユニオン型が作られるのです。

keyofは関数の引数に制限をかける際によく使います。以下にサンプルコードを示します。

function getValue(obj: Person, key: keyof Person) {  // 第二引数にkeyofを使用している(PersonKeysでもOK)
  return obj[key];
}

const taro: Person = {
  id: 1,
  name: 'Taro',
  email: 'taro@example.com',
};

getValue(taro, 'id'); // OK → 1
getValue(taro, 'email'); // OK → 'taro@example.com'
getValue(taro, "age"); // エラー:'age' は 'Person' に存在しないキー

上記のサンプルコードでは、オブジェクトから任意のキーの値を取得する関数getValueを作っており、keyof Personを使うことで、第二引数keyに指定できる値を「文字列"id""name""email"」のいずれかに限定させています。

存在しないキー名を指定するとコンパイル時にエラーになります。安全なプロパティアクセスが保証されるため、バグの予防につながります。

オプショナル(?)なプロパティもkeyofで列挙される

オプショナル(?)なプロパティもkeyofで列挙されます。以下にサンプルコードを示します。

type Person = {
  id: number;
  name: string;
  email?: string; // ← オプショナルプロパティ(省略可能)
};

type PersonKeys = keyof Person;
// 上は次と同じ意味になる
type PersonKeys = 'id' | 'name' | 'email';

emailは省略可能ですが、keyofで列挙されるということに注意してください。

keyofはinterfaceにも使える

keyofinterfaceに対しても同様に使えます。以下にサンプルコードを示します。

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

type PersonKeys = keyof Person;
// 上は次と同じ意味になる
type PersonKeys = 'id' | 'name' | 'email';

このように、typeinterfaceのどちらを使っていてもkeyofの動作は同じです。

typeofとは

typeofはJavaScriptにもありますが、TypeScriptでは変数や値の型を取得する演算子です。keyofは「型」に対して使用しましたが、typeofは「変数」や「定数」に対して使用します。

以下にサンプルコードを示します。

const person = {
  id: 1,
  name: 'Taro',
  email: 'taro@example.com',
};

type Person = typeof person;
// 上は次と同じ意味になる
type Person = {
  id: number;
  name: string;
  email: string;
};

このようにtypeofを使用すると、値から型を自動的に作ることができます。型定義を重複して書かなくてよいので便利です。

JavaScriptとTypeScriptでのtypeofの違い

JavaScriptのtypeof実行時値の型(文字列)を返しますが、TypeScriptのtypeof型定義の中で使い変数や値の「型」を返します。以下に違いを示します。

言語書き方(一例)役割戻り値
JavaScripttypeof person値の「型」を調べる(実行時)"string""number"などの型(文字列)
TypeScripttype Person = typeof person値の「型」を取得する(型定義)型そのもの(例:number型)

以下にJavaScriptとTypeScriptでtypeofを使用したサンプルコードを示します。

JavaScriptの例

const person = {
  id: 1,
  name: 'Taro',
  email: 'taro@example.com',
};
console.log(typeof person); // 'object'

TypeScriptの例

const person = {
  id: 1,
  name: 'Taro',
  email: 'taro@example.com',
};

type Person = typeof person;
// 上は次と同じ意味になる
type Person = {
  id: number;
  name: string;
  email: string;
};

// 以下は、JavaScriptのtypeofであり、実行時に型を判定します。
// personはオブジェクトなので 'object'という文字列が返されます。
console.log(typeof person); // 'object'

as const(constアサーション)と typeof の組み合わせ

TypeScriptでは、定数オブジェクトを使って型を定義するとき、値が自動的に一般的な型(stringnumber)として推論されてしまいます。定数オブジェクトの文字列や数値をそのまま型に反映したい場合はas constと組み合わせます。

as constを使わない場合

const COLORS = {
  RED: '#ff0000',
  GREEN: '#00ff00',
  BLUE: '#0000ff',
};

type ColorMap = typeof COLORS;
// 上は次と同じ意味になる
type ColorMap = {
  RED: string;
  GREEN: string;
  BLUE: string;
};

ColorMapの型を見ると、各値はすべてstring型として扱われていることが分かります。たとえ"#ff0000"という文字列を入れていても、それが固定されるわけではありません。

as constを使う場合

一方、as constを使うと以下のようになります。as constを使うと各値がリテラル型("#ff0000"など)として固定され、readonlyなプロパティになります。

const COLORS = {
  RED: '#ff0000',
  GREEN: '#00ff00',
  BLUE: '#0000ff',
} as const;

type ColorMap = typeof COLORS;
// 上は次と同じ意味になる
type ColorMap = {
  readonly RED: '#ff0000';
  readonly GREEN: '#00ff00';
  readonly BLUE: '#0000ff';
};

つまり、as constを使うと各プロパティの値がstringではなく、リテラル型の文字列として固定することができます。

keyof typeofでオブジェクトのキーから型を生成する

keyoftypeofを組み合わせると、「オブジェクトのプロパティ名(キー名)を列挙したユニオン型」を取得することができます。

なぜkeyof typeofを組み合わせるの?

keyofは「型」に対して使用するため、「変数」や「定数」には使用することができません。そのため、typeofで値から型を取得した後にkeyofで「取得した型のプロパティ名(キー名)を列挙したユニオン型」を取得する必要があります。

つまり、「変数」や「定数」からkeyofを使いたいときは、まずtypeofで型に変換する必要があるのです。

サンプルコードを以下に示します。

const COLORS = {
  RED: '#ff0000',
  GREEN: '#00ff00',
  BLUE: '#0000ff',
};

type ColorKeys = keyof typeof COLORS;
// 上は次と同じ意味になる
type ColorKeys = 'RED' | 'GREEN' | 'BLUE';

function getColorCode(color: ColorKeys): string {
  return COLORS[color];
}

getColorCode('RED'); // OK
getColorCode('BLACK'); // エラー! 'BLACK' は ColorKeysにはない

ColorKeysという型は「文字列"RED""GREEN""BLUE"」のどれかしか許されない型になります。

もう少し詳しく説明すると、まず定数COLORStypeofした時点では以下のオブジェクト型になっています。

const COLORS = {
  RED: '#ff0000',
  GREEN: '#00ff00',
  BLUE: '#0000ff',
};

type ColorMap = typeof COLORS;
// 上は次と同じ意味になる
type ColorMap = {
  RED: string;
  GREEN: string;
  BLUE: string;
};

このオブジェクト型ColorMapに対して、keyofを使用すると以下のようになります。

type ColorMap = {
  RED: string;
  GREEN: string;
  BLUE: string;
};

type ColorKeys = keyof ColorMap;
// 上は次と同じ意味になる
type ColorKeys = 'RED' | 'GREEN' | 'BLUE';

typeof Type[keyof typeof Type]でオブジェクトの値から型を生成する

先ほどはkeyof typeofを使って、オブジェクトの「キー名」から型を作る方法を紹介しました。今度は、オブジェクトの「値」から型を作る方法を説明します。

as consttypeof COLORS[keyof typeof COLORS]を組み合わせることで、オブジェクトの値から型を作ることができます。サンプルコードを以下に示します。

const COLORS = {
  RED: '#ff0000',
  GREEN: '#00ff00',
  BLUE: '#0000ff',
} as const; //as const を使うことで、各値('#ff0000' など)がリテラル型として扱われるようになります。

type ColorCode = (typeof COLORS)[keyof typeof COLORS];
// 上は次と同じ意味になる
type ColorCode = '#ff0000' | '#00ff00' | '#0000ff';

keyof typeof COLORSで「文字列"RED""GREEN""BLUE"」のどれかしか許されない型を取得しています。その後、typeof COLORS[keyof typeof COLORS]ですべてのキーに対応する「値の型」("#ff0000"など)をユニオン型として取得しています。

本記事のまとめ

この記事では、TypeScriptでよく使われるkeyoftypeofついて、以下のポイントを解説しました。

  • keyofはオブジェクト型のプロパティ名(キー名)を列挙したユニオン型として取得する
  • typeofは変数や値から型を取得する
  • keyof typeofで、オブジェクトのキー名をユニオン型として取得できる
  • typeof Type[keyof typeof Type]で、オブジェクトの値をユニオン型として取得できる
  • as constを使えば、リテラル型として扱うことができる

keyoftypeofをうまく使えば、型の定義がより明確になり、安全で保守しやすいコードが書けるようになります。

お読み頂きありがとうございました。

スポンサーリンク