TypeScript には keyof・typeof・インデックスアクセス型(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 |
T の K プロパティの型 |
プロパティ型を参照・Mapped Types |
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 は「K は T の存在するキーに限定する」という制約です。型安全なプロパティアクセス関数の実装に使われます。
// 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 でキーを 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 を型として使えるのは 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] |
ユーティリティ型(Pick・Record・Partial など)は内部でこれらの演算子を使っています。 ユーティリティ型完全ガイド も合わせて参照してください。
この3つの演算子をマスターすると、型定義の重複がなくなり「型が1か所変われば自動的に全体が追従する」設計が実現できます。まずは keyof と T[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 y は number です。型注釈で明示した型と 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] シグネチャを持つ型の keyof は string | number になります。通常のプロパティ名のみが必要な場合は string & keyof T で文字列キーに絞り込めます。
Qtypeof でクラスの型を取得すると InstanceType を使わないといけないのはなぜですか?
Atypeof MyClass はクラス自体の型(コンストラクタ関数の型)を返します。インスタンスの型が必要な場合は InstanceType<typeof MyClass> を使います。例えば DI コンテナで「クラスを受け取ってインスタンスを返す」関数を作る場合にnew () => T 型または InstanceType が必要になります。
Qkeyof any とはどういう意味ですか?
Akeyof any は string | number | symbol になります。JavaScript のオブジェクトキーとして有効な型の Union です。Record 型の実装 type Record<K extends keyof any, T> で使われており、「任意のキー型を受け取れる」ことを表現しています。実務では PropertyKey という組み込み型エイリアス(string | number | symbol)でも代替できます。
これらの型演算子は単体でも強力ですが、組み合わせることでその真価を発揮します。keyof・typeof・インデックスアクセス型をセットで覚え、ジェネリクスや Mapped Types と組み合わせてみてください。TypeScript の型システムが持つ表現力の高さを実感できるはずです。

