TypeScript でコードを書いていると、「この型のこのプロパティと同じ型を、別の場所でも使いたい」という場面がよくあります。そんなときに便利なのが Indexed Access Types(インデックスアクセス型/ルックアップ型)です。Person["name"] のように、既存の型に角括弧でキーを指定するだけで、その部分の型を取り出せます。値ではなく型に対して行う操作なので、元の型を変えれば取り出した型も自動で追従します。この記事では、基本の T[K] から、keyof や配列の number との組み合わせ、typeof で値から型を導く方法まで、初心者〜中級者向けに解説します。
目次
T[K] でプロパティの型を取り出す
まずは基本形です。オブジェクト型に対して角括弧でプロパティ名(キー)を渡すと、そのプロパティの型が得られます。値へのアクセスと同じ記法ですが、書いている場所が「型の文脈」である点が違います。キーは .name のようなドット記法ではなく、必ず ["name"] のように文字列リテラルの角括弧で指定します。
type Person = {
name: string;
age: number;
isAdmin: boolean;
};
// Person の name プロパティの型を取り出す
type Name = Person["name"]; // string
type Age = Person["age"]; // number
Person["name"] は string、Person["age"] は number になります。ポイントは、Name を string と直接書くのではなく Person["name"] と書いておくことです。こうしておくと、あとで Person の name を別の型に変えたとき、Name も自動的にその型に変わります。型定義を一箇所にまとめておけるのがインデックスアクセス型の利点です。
複数のキーをまとめて取り出す
角括弧の中にユニオン型を渡すと、複数のプロパティの型をまとめて取り出せます。結果は、それぞれのプロパティ型を合わせたユニオン型になります。
type Person = {
name: string;
age: number;
isAdmin: boolean;
};
// name と age、2つの型のユニオンを取り出す
type NameOrAge = Person["name" | "age"]; // string | number
Person["name" | "age"] は string | number になります。name が string、age が number なので、その2つを合わせたユニオン型が返るわけです。「いくつかのプロパティのどれか」を表したいときに使えます。
keyof と組み合わせて全プロパティの型を得る
keyof は、オブジェクト型からすべてのキー名をユニオン型として取り出す演算子です。これをインデックスアクセス型の角括弧に渡すと、「すべてのプロパティの型のユニオン」が得られます。Person[keyof Person] と書くと、その型が持つ値の型をまとめて表現できます。
type Person = {
name: string;
age: number;
isAdmin: boolean;
};
// keyof Person は "name" | "age" | "isAdmin"
type PersonKeys = keyof Person;
// すべてのプロパティの型のユニオンを取り出す
type PersonValues = Person[keyof Person]; // string | number | boolean
keyof Person は "name" | "age" | "isAdmin" というキーのユニオンです。それを角括弧に渡した Person[keyof Person] は、各プロパティの型を合わせた string | number | boolean になります。オブジェクトの「値としてあり得る型」を一括で表したいときの定番パターンです。
配列の要素型を T[number] で取り出す
インデックスアクセス型は配列やタプルにも使えます。配列型に [number] を渡すと、その配列の「要素の型」が取り出せます。配列の要素は数値インデックスでアクセスするため、キーとして number を指定するわけです。
type Users = { id: number; name: string }[];
// 配列の要素1つ分の型を取り出す
type User = Users[number]; // { id: number; name: string }
// タプルなら数値リテラルで特定の位置の型も取れる
type Pair = [string, number];
type First = Pair[0]; // string
type Second = Pair[1]; // number
Users[number] は、配列 Users の要素1つ分の型である { id: number; name: string } になります。配列型だけを定義しておけば、その要素の型は T[number] で導けるので、要素の型を別途書く必要がありません。タプルの場合は Pair[0] のように数値リテラルを渡すと、その位置の型をピンポイントで取り出せます。
typeof と組み合わせて値から型を導く
ここまではあらかじめ用意した型に対して操作してきましたが、実際の開発では「定数オブジェクトが先にあって、そこから型を導きたい」という場面が多くあります。値に対して typeof を付けると、その値の型が得られます。これをインデックスアクセス型と組み合わせると、定数オブジェクトから型を作り出せます。
// as const でリテラル型を保ったまま定数を定義
const ROLES = {
admin: "管理者",
editor: "編集者",
viewer: "閲覧者",
} as const;
// オブジェクトのキーのユニオンを取り出す
type RoleKey = keyof typeof ROLES; // "admin" | "editor" | "viewer"
// オブジェクトの値のユニオンを取り出す
type RoleLabel = (typeof ROLES)[keyof typeof ROLES];
// "管理者" | "編集者" | "閲覧者"
typeof ROLES で定数 ROLES の型を取り出し、keyof typeof ROLES でそのキー "admin" | "editor" | "viewer" を得ています。さらに (typeof ROLES)[keyof typeof ROLES] と書くと、値側の "管理者" | "編集者" | "閲覧者" が取り出せます。as const を付けているため値がリテラル型のまま保たれ、こうした厳密な型が導けます。定数を1つ書けば、そこからキーの型も値の型も自動で生成できるのが大きな利点です。
関数の引数をプロパティの型で受ける
インデックスアクセス型は、関数の引数の型注釈でも力を発揮します。ある型のプロパティと同じ型を引数に受けたいとき、型を直接書くのではなく T[K] で参照しておくと、元の型が変わっても引数の型が自動的に追従します。
type Product = {
id: string;
price: number;
status: "draft" | "published" | "archived";
};
// status プロパティと同じ型を引数に受ける
function updateStatus(id: Product["id"], status: Product["status"]): void {
console.log(`${id} を ${status} に更新`);
}
updateStatus("p-001", "published"); // OK
updateStatus("p-002", "deleted"); // エラー:status に無い値
引数 status の型を Product["status"] にしているため、渡せる値は "draft" | "published" | "archived" のいずれかに限定されます。"deleted" のような定義外の値を渡すとコンパイルエラーになります。もし Product の status に新しい値を追加すれば、この関数が受け付ける値も自動的に増えます。型の定義元を Product に一本化できるわけです。
ネストしたプロパティの型を取り出す
角括弧は連結できるので、入れ子になったオブジェクト型でも奥のプロパティまで辿って型を取り出せます。T["a"]["b"] のように、階層の分だけ角括弧を重ねます。
type ApiResponse = {
data: {
user: {
id: number;
profile: { bio: string; url: string };
};
};
};
// 奥のプロパティまで辿って型を取り出す
type Profile = ApiResponse["data"]["user"]["profile"];
// { bio: string; url: string }
type Bio = ApiResponse["data"]["user"]["profile"]["bio"]; // string
ApiResponse["data"]["user"]["profile"] のように角括弧をつなげると、深い階層の型 { bio: string; url: string } に辿り着けます。API のレスポンス型のように入れ子が深い型から、一部だけを取り出して使い回したいときに便利です。
型が取り出せずエラーになるとき
インデックスアクセス型で意図した型が得られない、あるいはエラーになる場合、原因はキーの指定方法か値の広がり方にあることがほとんどです。
存在しないキーを指定している
角括弧に渡せるのは、その型に実際に存在するキーだけです。Person["nickname"] のように定義されていないプロパティ名を渡すと、「プロパティが存在しない」という趣旨のエラーになります。存在するかどうか不安なときは、keyof Person で有効なキーの一覧を確認するとよいでしょう。なお、キーはドット記法では書けないため、Person.name ではなく必ず Person["name"] と角括弧で書く点にも注意してください。
as const を付け忘れてリテラル型が失われる
定数オブジェクトから typeof で型を導くとき、as const を付けないと文字列プロパティは string に広がってしまいます。たとえば as const の無い ROLES から (typeof ROLES)[keyof typeof ROLES] を取り出すと、期待した "管理者" | "編集者" ではなく単なる string になります。値をリテラル型のまま保ちたいときは、オブジェクトに as const を付けるのを忘れないようにします。
まとめ
Indexed Access Types(インデックスアクセス型)は、Person["name"] のように既存の型へ角括弧でキーを渡し、その部分の型を取り出す機能です。Person["name" | "age"] で複数プロパティをまとめて、Person[keyof Person] で全プロパティの型を、T[number] で配列の要素型を取り出せます。さらに typeof と組み合わせれば、as const で定義した定数オブジェクトからキーや値の型を導き出せます。共通するのは「型を二重に書かず、定義元を一箇所にまとめられる」という利点です。関数の引数やネストした型の一部を扱うときにも役立つので、型の重複が気になったらぜひ使ってみてください。