【TypeScript】keyof・typeof・インデックスアクセス型 完全ガイド|型演算子の仕組みと実務パターンを徹底解説

【TypeScript】keyof・typeof・インデックスアクセス型 完全ガイド|型演算子の仕組みと実務パターンを徹底解説 TypeScript

TypeScript には keyoftypeof・インデックスアクセス型(T[K])という3つの強力な型演算子があります。これらを使いこなすと、型の重複定義をなくし・変更に強く・補完が効くコードが書けます。

本記事では各演算子の仕組みを基礎から解説し、ジェネリクス制約・Mapped Types・ユーティリティ型の自作など実務で使えるパターンを実例付きで紹介します。

この記事でわかること

  • keyof でオブジェクト型のキーを Union 型として取得する方法
  • typeof で変数・定数・関数から型を取得する方法
  • インデックスアクセス型(T[K])でプロパティの型を参照する方法
  • ジェネリクス制約 K extends keyof T で型安全な関数を作る方法
  • Mapped Types・テンプレートリテラル型との組み合わせパターン
  • 実践パターン3本・FAQ6問
スポンサーリンク

1. 3つの型演算子を比較する

演算子 入力 出力 主な用途
keyof T オブジェクト型 T キー名の Union 型 型安全なプロパティアクセス・ジェネリクス制約
typeof x 変数・定数・関数 x x の型 値から型を作る・ReturnType/Parameters と組み合わせ
T[K] T とキー型 K TK プロパティの型 プロパティ型を参照・Mapped Types
3つは組み合わせて使うことが多い
keyof T でキーを取得し T[K] で値の型を参照するパターン、typeof obj で型を作ってから keyof を適用するパターンなど、組み合わせることで強力な型安全性が生まれます。

2. keyof 型演算子

2-1. 基本:オブジェクト型のキーを Union 型として取得

keyof T は型 T の全プロパティ名を文字列リテラルの Union 型として返します。

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

type UserKeys = keyof User;
// UserKeys = "id" | "name" | "email" | "age"

// number インデックスシグネチャを持つ型
interface NumberMap {
    [key: number]: string;
}
type NMKeys = keyof NumberMap; // number

// string インデックスシグネチャを持つ型
interface StringMap {
    [key: string]: unknown;
}
type SMKeys = keyof StringMap; // string | number
// ※ JS ではオブジェクトキーを number でアクセスしても string 扱いのため

2-2. ジェネリクス制約 K extends keyof T

K extends keyof T は「KT の存在するキーに限定する」という制約です。型安全なプロパティアクセス関数の実装に使われます。

// T のプロパティ名で絞り込まれた型安全な getter
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

const user: User = { id: 1, name: "Alice", email: "alice@example.com", age: 30 };

const name = getProperty(user, "name");   // 型: string
const id   = getProperty(user, "id");     // 型: number
// getProperty(user, "phone");            // Error: "phone" は keyof User にない

// 複数プロパティを選択して新しいオブジェクトを作る
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
    const result = {} as Pick<T, K>;
    keys.forEach(key => { result[key] = obj[key]; });
    return result;
}

const partial = pick(user, ["name", "email"]);
// 型: { name: string; email: string }

2-3. keyof とテンプレートリテラル型の組み合わせ

// プロパティ名から getter メソッド名を生成する型
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Product {
    name: string;
    price: number;
    stock: number;
}

type ProductGetters = Getters<Product>;
// {
//   getName: () => string;
//   getPrice: () => number;
//   getStock: () => number;
// }

// イベントハンドラ型を自動生成
type EventHandlers<T> = {
    [K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void;
};

type UserHandlers = EventHandlers<User>;
// { onIdChange: (value: number) => void; onNameChange: (value: string) => void; ... }

3. typeof 型演算子

型コンテキストで使う typeof は JavaScript の typeof 演算子(実行時)とは別物で、変数・定数・関数から TypeScript の型情報を取得します。

3-1. 変数・定数から型を取得

// 変数から型を取得
const config = {
    host: "localhost",
    port: 3000,
    debug: false,
};

type Config = typeof config;
// Config = { host: string; port: number; debug: boolean }

// as const との組み合わせでリテラル型に
const COLORS = ["red", "green", "blue"] as const;
type Color = typeof COLORS[number];
// Color = "red" | "green" | "blue"

// enum の代替パターン(as const オブジェクト)
const Direction = {
    UP: "UP",
    DOWN: "DOWN",
    LEFT: "LEFT",
    RIGHT: "RIGHT",
} as const;

type Direction = typeof Direction[keyof typeof Direction];
// Direction = "UP" | "DOWN" | "LEFT" | "RIGHT"
as const + typeof + keyof の黄金トリオ
as const でリテラル型を保持 → typeof で型を取得 → keyof でキーを Union 化 という組み合わせは、enum を使わずに型安全な定数を定義するベストプラクティスです。as const の詳細は 型推論完全ガイド も参照してください。

3-2. 関数から型を取得(ReturnType / Parameters)

// 関数の型を取得
async function fetchUser(id: number) {
    const res = await fetch(`/api/users/${id}`);
    return res.json() as Promise<{ id: number; name: string; role: string }>;
}

// typeof + ReturnType で戻り値型を取得
type FetchUserReturn = Awaited<ReturnType<typeof fetchUser>>;
// FetchUserReturn = { id: number; name: string; role: string }

// typeof + Parameters で引数型を取得
type FetchUserParams = Parameters<typeof fetchUser>;
// FetchUserParams = [id: number]

// 実用例: ラッパー関数に同じ型シグネチャを持たせる
function cachedFetchUser(...args: Parameters<typeof fetchUser>): ReturnType<typeof fetchUser> {
    // キャッシュロジックを追加
    return fetchUser(...args);
}

3-3. クラスから型を取得

class UserService {
    getUser(id: number): Promise<User> { return fetch(`/api/users/${id}`).then(r => r.json()); }
    createUser(data: Omit<User, "id">): Promise<User> { return fetch("/api/users", { method: "POST" }).then(r => r.json()); }
}

// クラスのインスタンス型
type UserServiceInstance = InstanceType<typeof UserService>;
// UserService のインスタンスが持つメソッド・プロパティの型

// クラス自体の型(コンストラクタ型)
type UserServiceClass = typeof UserService;

// DI コンテナのようなパターン
function createService<T>(ServiceClass: new () => T): T {
    return new ServiceClass();
}
const service = createService(UserService); // 型: UserService
typeof は型コンテキストでのみ使える
typeof を型として使えるのは type 宣言・型注釈・ジェネリクス引数など型コンテキスト内のみです。値コンテキスト(式の中)では JavaScript の実行時 typeof(”string”, “number” などを返す)として動作します。混同しないよう注意してください。

4. インデックスアクセス型(Lookup 型)

T[K] 記法で型 T のプロパティ K の型を参照できます。これをインデックスアクセス型(または Lookup 型)と呼びます。

4-1. オブジェクト型のプロパティ型を参照

interface Order {
    id: number;
    status: "pending" | "shipped" | "delivered" | "cancelled";
    items: Array<{ productId: number; quantity: number; price: number }>;
    address: { street: string; city: string; zip: string };
}

// プロパティ型を参照
type OrderStatus = Order["status"];
// "pending" | "shipped" | "delivered" | "cancelled"

type OrderItems = Order["items"];
// Array<{ productId: number; quantity: number; price: number }>

type OrderAddress = Order["address"];
// { street: string; city: string; zip: string }

// ネストしたプロパティ型
type CityType = Order["address"]["city"];
// string

// Union キーで複数プロパティの型をまとめて取得
type IdOrStatus = Order["id" | "status"];
// number | "pending" | "shipped" | "delivered" | "cancelled"

4-2. 配列・タプルの要素型を取得

// 配列の要素型を取得(number インデックス)
type Items = Order["items"];
type Item = Items[number];
// { productId: number; quantity: number; price: number }

// 省略記法
type OrderItem = Order["items"][number];

// タプルの特定位置の型を取得
type Pair = [string, number, boolean];
type First  = Pair[0]; // string
type Second = Pair[1]; // number
type Third  = Pair[2]; // boolean

// タプル全要素の Union 型
type PairElements = Pair[number]; // string | number | boolean

// as const 配列の要素型
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = typeof ROLES[number]; // "admin" | "editor" | "viewer"

4-3. keyof と組み合わせた応用パターン

// T の任意のプロパティ値の型 → ValueOf<T>
type ValueOf<T> = T[keyof T];

interface Theme {
    primary: string;
    secondary: string;
    accent: string;
    fontSize: number;
}

type ThemeValue = ValueOf<Theme>; // string | number

// 特定型のプロパティ名のみ抽出(文字列値を持つキー)
type StringKeys<T> = {
    [K in keyof T]: T[K] extends string ? K : never;
}[keyof T];

type ThemeStringKeys = StringKeys<Theme>;
// "primary" | "secondary" | "accent"(fontSize は除外)

// 深いネストを段階的に取得
interface ApiResponse {
    data: {
        users: Array<{ id: number; profile: { avatar: string } }>;
    };
    meta: { total: number; page: number };
}

type AvatarType = ApiResponse["data"]["users"][number]["profile"]["avatar"];
// string

5. 組み合わせパターン

5-1. Mapped Types での keyof + T[K]

// 標準の Readonly<T> の実装イメージ
type MyReadonly<T> = {
    readonly [K in keyof T]: T[K];
};

// 標準の Partial<T> の実装イメージ
type MyPartial<T> = {
    [K in keyof T]?: T[K];
};

// プロパティを Promise でラップする型
type Promisify<T> = {
    [K in keyof T]: Promise<T[K]>;
};

type AsyncUser = Promisify<User>;
// { id: Promise<number>; name: Promise<string>; email: Promise<string>; age: Promise<number> }

// 関数型プロパティのみを残す型
type FunctionProps<T> = {
    [K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : never;
};

5-2. typeof + keyof で設定駆動型の型を作る

// 設定オブジェクトから型を自動生成するパターン
const ENDPOINTS = {
    users:    "/api/users",
    products: "/api/products",
    orders:   "/api/orders",
} as const;

type EndpointKey = keyof typeof ENDPOINTS;
// "users" | "products" | "orders"

type EndpointPath = typeof ENDPOINTS[EndpointKey];
// "/api/users" | "/api/products" | "/api/orders"

// 型安全な API クライアント
async function apiGet<K extends keyof typeof ENDPOINTS>(
    key: K
): Promise<unknown> {
    const url = ENDPOINTS[key]; // 型: "/api/users" | "/api/products" | "/api/orders"
    const res = await fetch(url);
    return res.json();
}

apiGet("users");    // OK
// apiGet("posts"); // Error: "posts" は keyof typeof ENDPOINTS にない

6. 実践例3本

実践例1:フォームバリデーションの型安全な実装

フォームの各フィールドに対するバリデーションルールを型安全に定義するパターンです。

interface SignupForm {
    username: string;
    email: string;
    password: string;
    age: number;
}

// keyof と T[K] でバリデーションルール型を作る
type ValidationRule<T> = {
    [K in keyof T]?: {
        required?: boolean;
        validate?: (value: T[K]) => string | null; // エラーメッセージ or null
    };
};

const rules: ValidationRule<SignupForm> = {
    username: {
        required: true,
        validate: (v) => v.length >= 3 ? null : "3文字以上必要です",
    },
    email: {
        required: true,
        validate: (v) => v.includes("@") ? null : "正しいメールアドレスを入力してください",
    },
    age: {
        validate: (v) => v >= 18 ? null : "18歳以上である必要があります",
    },
};

// バリデーション実行関数
function validate<T>(data: T, rules: ValidationRule<T>): Partial<Record<keyof T, string>> {
    const errors: Partial<Record<keyof T, string>> = {};
    (Object.keys(rules) as (keyof T)[]).forEach(key => {
        const rule = rules[key];
        if (!rule) return;
        if (rule.required && !data[key]) {
            (errors as any)[key] = "必須項目です";
        } else if (rule.validate) {
            const error = rule.validate(data[key]);
            if (error) (errors as any)[key] = error;
        }
    });
    return errors;
}

実践例2:型安全なイベントエミッターの実装

イベント名とペイロード型のマッピングを定義し、on/emit を完全に型安全にするパターンです。

// イベントマップ定義
interface AppEvents {
    userLogin:    { userId: string; timestamp: Date };
    userLogout:   { userId: string };
    dataUpdated:  { resource: string; id: number };
    errorOccurred: { message: string; code: number };
}

// keyof + T[K] で型安全なエミッタークラス
class TypedEventEmitter<Events extends Record<string, unknown>> {
    private listeners = new Map<keyof Events, Set<Function>>();

    on<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void {
        if (!this.listeners.has(event)) this.listeners.set(event, new Set());
        this.listeners.get(event)!.add(listener);
    }

    emit<K extends keyof Events>(event: K, payload: Events[K]): void {
        this.listeners.get(event)?.forEach(fn => fn(payload));
    }

    off<K extends keyof Events>(event: K, listener: (payload: Events[K]) => void): void {
        this.listeners.get(event)?.delete(listener);
    }
}

const emitter = new TypedEventEmitter<AppEvents>();

emitter.on("userLogin", ({ userId, timestamp }) => {
    console.log(`${userId} logged in at ${timestamp}`);
});

emitter.emit("userLogin", { userId: "alice", timestamp: new Date() }); // OK
// emitter.emit("userLogin", { userId: "alice" });                     // Error: timestamp がない
// emitter.on("unknown", () => {});                                    // Error: 未定義イベント

実践例3:オブジェクトの深い型変換ユーティリティ

keyof と再帰型を組み合わせて、オブジェクトのすべてのプロパティに型変換を適用するユーティリティです。

// Deep Readonly: ネストしたオブジェクトをすべて readonly にする
type DeepReadonly<T> = {
    readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface AppState {
    user: { id: number; name: string; settings: { theme: string; lang: string } };
    cart: { items: Array<{ id: number; qty: number }>; total: number };
}

type ReadonlyState = DeepReadonly<AppState>;
// すべてのプロパティが再帰的に readonly になる

// Deep Partial: すべてを省略可能にする
type DeepPartial<T> = {
    [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

function mergeState<T>(base: T, patch: DeepPartial<T>): T {
    const result = { ...base };
    (Object.keys(patch) as (keyof T)[]).forEach(key => {
        const val = patch[key];
        if (val !== undefined && typeof val === "object" && !Array.isArray(val)) {
            result[key] = mergeState(base[key] as any, val as any);
        } else if (val !== undefined) {
            result[key] = val as T[typeof key];
        }
    });
    return result;
}

// 型安全な部分更新
const newState = mergeState(initialState, {
    user: { settings: { theme: "dark" } },  // 深い部分のみ更新
});

Mapped Types の詳細は 高度な型完全ガイド、ジェネリクスの詳細は ジェネリクス完全ガイド を参照してください。

7. まとめ:使い分け早見表

やりたいこと 使う演算子
オブジェクト型のキーを Union 型で取得 keyof T keyof User"id" | "name" | ...
ジェネリクスのプロパティ制約 K extends keyof T function get<T, K extends keyof T>(obj: T, k: K)
変数・定数から型を取得 typeof x type Config = typeof config
関数の戻り値型を取得 ReturnType<typeof fn> type R = ReturnType<typeof fetchUser>
プロパティの型を参照 T[K] Order["status"] → ステータス Union 型
配列要素の型を取得 T[number] typeof ROLES[number]
as const + keyof で Union 型を生成 typeof x[keyof typeof x] Direction = typeof Direction[keyof typeof Direction]

ユーティリティ型(PickRecordPartial など)は内部でこれらの演算子を使っています。 ユーティリティ型完全ガイド も合わせて参照してください。

この3つの演算子をマスターすると、型定義の重複がなくなり「型が1か所変われば自動的に全体が追従する」設計が実現できます。まずは keyofT[K] の組み合わせから実際に書いてみましょう。

FAQ

Qkeyof と Object.keys() の違いは何ですか?

Akeyof T型レベルの演算子でコンパイル時に型情報を生成します。Object.keys()実行時にオブジェクトのキーを文字列配列で返します。TypeScript では Object.keys(obj) の戻り値型は string[](型情報が失われる)のため、型安全なキーアクセスには keyof を使ったアプローチが必要です。

Qtypeof を型として使うとき「typeof 変数名」と「変数名の型」は同じですか?

A多くの場合は同じですが、const 宣言と let 宣言で推論される型が異なります。const x = 42 の場合、変数 x の型は 42(リテラル型)ですが、let y = 42 の場合 typeof ynumber です。型注釈で明示した型と typeof で取得した型が一致するかは場合によります。

QT[K] で「型 K は型 T のインデックス型として使用できません」というエラーが出ます。

AT[K] を使うには K extends keyof T という制約が必要です。ジェネリクス関数では function f<T, K extends keyof T>(obj: T, key: K): T[K] のように書きます。また、K が具体的な文字列の場合は T にそのキーが存在するか確認してください。

Qkeyof でインデックスシグネチャを持つ型のキーが string | number になるのはなぜですか?

AJavaScript ではオブジェクトのキーを数値で指定しても内部では文字列として扱われます(例: obj[1]obj["1"] と同じ)。そのため [key: string] シグネチャを持つ型の keyofstring | number になります。通常のプロパティ名のみが必要な場合は string & keyof T で文字列キーに絞り込めます。

Qtypeof でクラスの型を取得すると InstanceType を使わないといけないのはなぜですか?

Atypeof MyClass はクラス自体の型(コンストラクタ関数の型)を返します。インスタンスの型が必要な場合は InstanceType<typeof MyClass> を使います。例えば DI コンテナで「クラスを受け取ってインスタンスを返す」関数を作る場合にnew () => T 型または InstanceType が必要になります。

Qkeyof any とはどういう意味ですか?

Akeyof anystring | number | symbol になります。JavaScript のオブジェクトキーとして有効な型の Union です。Record 型の実装 type Record<K extends keyof any, T> で使われており、「任意のキー型を受け取れる」ことを表現しています。実務では PropertyKey という組み込み型エイリアス(string | number | symbol)でも代替できます。

これらの型演算子は単体でも強力ですが、組み合わせることでその真価を発揮します。keyoftypeof・インデックスアクセス型をセットで覚え、ジェネリクスや Mapped Types と組み合わせてみてください。TypeScript の型システムが持つ表現力の高さを実感できるはずです。