Menu

TypeScriptのkeyof: オブジェクトのキーを型として取得する

keyof はオブジェクト型を受け取り、そのプロパティ名のユニオン型を返します。インターフェースに対する keyof、普通のオブジェクトに対する keyof typeof、ジェネリクスを使った型付きのプロパティアクセス、インデックスシグネチャ(string | number)、Object.keys が string[] を返す理由を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

keyof はオブジェクト型を受け取り、そのプロパティ名のユニオン型を作ります。下の keyof User は型 "id" | "name" | "email" なので、その型の変数はこの3つの文字列のどれかしか持てません。

keyof は型システムの中にしか存在しません。コードのコンパイル時に消去されるので、実行時に何かをチェックすることはありません。与えてくれるのは、キーが本当に存在するプロパティを指しているというコンパイル時の保証です。インターフェースで email の名前を変えると、keyof User と型付けされたすべての "email" がエラーになります。

keyof typeof: 普通のオブジェクトのキー

keyof は値ではなく型に働きます。値として書いたオブジェクトなら、先に typeof で型を得てから、キーを取り出します:

内側から読みましょう。typeof colors は { red: string; green: string; blue: string } で、その keyof がキーのユニオン型です。typeof なしで keyof colors と書くと、error TS2749: 'colors' refers to a value, but is being used as a type here. Did you mean 'typeof colors'? で失敗します。

これがルックアップテーブルからユニオン型を作る定番の方法です。オブジェクトに色を追加すれば、Color 型もそれに合わせて増えます。typeof には実行時の意味もあり、typeof のページで両方を扱っています。

ジェネリクスを使った型付きのプロパティアクセス

keyof のいちばんよくある使い方はジェネリクスの制約です。K extends keyof T は「K は T のキーのどれか」という意味で、T[K] はそのキーのプロパティの型です:

2つ目の型パラメーターには意味があります。K を使わずに key: keyof T とすると、戻り値の型は T[keyof T]、つまりすべてのプロパティの型のユニオン(ここでは string | number | boolean)になるので、title.toUpperCase() は error TS2339: Property 'toUpperCase' does not exist on type 'string | number | boolean' で失敗します。正確なキーを K で捉えると、正確なプロパティの型が保たれます。T[K] はインデックスアクセス型で、インデックスアクセス型のページで説明しています。

普通の string でインデックスアクセスする

どこかから来た string でオブジェクトにインデックスアクセスすると、とてもよく見るエラーが出ます:

コンパイラーの出力は次のとおりです:

index.ts(10,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Settings'.
  No index signature with a parameter of type 'string' was found on type 'Settings'.

read には "colour" を含むどんな文字列でも届く可能性があり、Settings はそのキーについて何も言っていません。直し方は2つあります。本当のキーだけを受け付けるか、知らない文字列を使う前にチェックするかです。

name is keyof Settings は型述語です。関数が true を返すと、if の中でTypeScriptは input をキーとして扱います。型述語については 型ガードのページ で詳しく説明しています。

インデックスシグネチャと keyof: string | number

インデックスシグネチャを持つ型に対して、keyof はインデックスの型を返し、文字列のインデックスシグネチャなら string だけではなく string | number を返します:

JavaScriptは数値のプロパティキーをすべて文字列に変換するので、scores[42] と scores["42"] は同じプロパティです。TypeScriptはそれを number を含めることで表しています。出力はJavaScriptのルールも示しています。整数のようなキーは、ほかの文字列のキーより前に昇順で並びます。ジェネリック関数で文字列のキーだけが必要なら、Extract<keyof T, string> か string & keyof T と書きます。Exclude と Extract には専用のページがあります。

keyof の結果の一覧

型keyof の結果
{ id: number; name: string }"id" | "name"
{ [key: string]: number }string | number
{ [index: number]: string }number
{}never
anystring | number | symbol
{ a: 1; b: 2 } | { a: 3; c: 4 }"a"(すべてのメンバーが持つキー)
{ a: 1 } & { c: 4 }"a" | "c"(どちらかが持つキー)
string[]number と配列のすべてのメソッド名("length"、"push" など)

ユニオン型と交差型の行は、最初は逆に見えます。ユニオン型の値はどちらのメンバーでもありうるので、安全なのは両方にあるキーだけです。交差型の値は両方のすべてを持っているので、すべてのキーを持ちます。

Object.keys が string[] を返す理由

Object.keys(obj) の型は string[] で、(keyof T)[] になることはありません。理由は構造的型付けです。値は、宣言された型より多くのプロパティを持てます。

p の型は Point ですが、実行時には z というキーを持っています。Object.keys が ("x" | "y")[] を約束したら、それに頼るコードは p に対して間違ってしまいます。自分で作ったオブジェクトリテラルならキャストしても安全です。外から届くオブジェクトなら、キーは string のままにしてチェックしましょう。

マップ型の中の keyof

keyof は、型のキーをループして新しい型を作る方法でもあります。組み込みのユーティリティ型 Partial と Readonly はこの方法で書かれています:

type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };

interface User {
  id: number;
  name: string;
}

type UserDraft = MyPartial<User>; // { id?: number; name?: string }

[K in keyof T] は各キーを順に訪れ、T[K] はそのキーのプロパティの型を読みます。マップ型には、修飾子やキーの再マッピングを扱う専用のページがあります。

よくある質問

TypeScriptの keyof は何をしますか?

keyof T は、型 T のプロパティ名のユニオン型を作ります。interface User { id: number; name: string } なら、keyof User は "id" | "name" です。型にだけ、コンパイル時にだけ働き、JavaScriptは何も生成しません。

TypeScriptの keyof typeof とは何ですか?

keyof は型を必要としますが、const colors = { red: "#f00" } のような普通のオブジェクトは値です。typeof colors が値を型に変え、keyof typeof colors がそのキーのユニオン型、ここでは "red" を返します。keyof colors と書くとエラー TS2749 で失敗します。

keyof が string | number を返すのはなぜですか?

型が { [key: string]: number } のような文字列のインデックスシグネチャを持つときです。JavaScriptは数値のプロパティキーを文字列に変換するので、obj[42] も正しいアクセスであり、TypeScriptはキーの型に number を含めます。文字列のキーだけが欲しいなら、Extract<keyof T, string> か string & keyof T を使います。

Object.keys が (keyof T)[] ではなく string[] を返すのはなぜですか?

オブジェクトは型に書かれている以上のプロパティを持てるからです。余分なフィールドを持つ値も、より狭い型に代入できます。結果を (keyof T)[] と型付けすると、そうしたオブジェクトについては嘘になります。オブジェクトが宣言どおりのキーだけを持つとわかっているなら、キャストします: Object.keys(obj) as (keyof typeof obj)[]。

「Element implicitly has an 'any' type because expression of type 'string' can't be used to index type」を直すには?

インデックスが普通の string で、オブジェクトの型に文字列のインデックスシグネチャがないのが原因です。引数を string ではなく keyof YourType と型付けするか、先に文字列を keyof YourType に絞り込む型ガードでチェックするか、本当に任意のキーを受け付けるならオブジェクトにインデックスシグネチャか Record<string, V> 型を与えます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める