TypeScriptの型システムを理解するうえで、避けて通れないのがkeyof
やtypeof
といった演算子です。
でも、初めて見ると…
- これって何をしてるの?
- どういう場面で使うの?
- JavaScriptの
typeof
とは何が違うの?
といった疑問が浮かんでくるかもしれません。
この記事では、TypeScriptでよく使われるkeyof
とtypeof
について、以下の内容をサンプルコードを用いてわかりやすく解説します。
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にも使える
keyof
はinterface
に対しても同様に使えます。以下にサンプルコードを示します。
interface Person {
id: number;
name: string;
age: number;
}
type PersonKeys = keyof Person;
// 上は次と同じ意味になる
type PersonKeys = 'id' | 'name' | 'email';
このように、type
とinterface
のどちらを使っていても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
は型定義の中で使い、変数や値の「型」を返します。以下に違いを示します。
言語 | 書き方(一例) | 役割 | 戻り値 |
---|---|---|---|
JavaScript | typeof person | 値の「型」を調べる(実行時) | "string" や"number" などの型(文字列) |
TypeScript | type 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では、定数オブジェクトを使って型を定義するとき、値が自動的に一般的な型(string
やnumber
)として推論されてしまいます。定数オブジェクトの文字列や数値をそのまま型に反映したい場合は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でオブジェクトのキーから型を生成する
keyof
とtypeof
を組み合わせると、「オブジェクトのプロパティ名(キー名)を列挙したユニオン型」を取得することができます。
なぜ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"
」のどれかしか許されない型になります。
もう少し詳しく説明すると、まず定数COLORS
をtypeof
した時点では以下のオブジェクト型になっています。
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 const
とtypeof 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でよく使われるkeyof
とtypeof
ついて、以下のポイントを解説しました。
keyof
はオブジェクト型のプロパティ名(キー名)を列挙したユニオン型として取得するtypeof
は変数や値から型を取得するkeyof typeof
で、オブジェクトのキー名をユニオン型として取得できるtypeof Type[keyof typeof Type]
で、オブジェクトの値をユニオン型として取得できるas const
を使えば、リテラル型として扱うことができる
keyof
やtypeof
をうまく使えば、型の定義がより明確になり、安全で保守しやすいコードが書けるようになります。
お読み頂きありがとうございました。