- TypeScript
TypeScript keyof typeofの使い方|キーと値の型を作る
keyof typeof とは、オブジェクトの「値」から、そのキーのユニオン型("a" | "b" のような型)を作る書き方です。 typeof で値から型を取り出し、keyof でその型のキーを取り出す、という2段階になっています。値のほうの型が欲しいときは、T[K] と書くインデックスアクセス型を組み合わせます。
この記事では、keyof・typeof・keyof typeof・T[K]・T[keyof T]・(typeof arr)[number] の結果を「入力と得られる型」の対応表にまとめます。さらに、as const の有無で値の型が string に広がる様子を並べて見せ、enum の代わりになる定数オブジェクト、Object.keys が string[] になる理由と型安全な回避コード、satisfies との併用まで、TypeScript 7.0.2 で実際に出した型とエラー文つきで解説します。
keyof typeofの早見表(入力と得られる型)
次の5つの定義に対して、それぞれの書き方で得られる型を並べました。
const config = { host: "localhost", port: 5432, debug: true };
const STATUS = { draft: "下書き", published: "公開中", archived: "保管" } as const;
const roles = ["admin", "member", "guest"] as const;
type User = { id: number; name: string; email?: string };
const users = [{ id: 1, name: "佐藤" }, { id: 2, name: "鈴木" }];
| やりたいこと | 書き方 | 得られる型 |
|---|---|---|
| 値から型を取り出す | typeof config |
{ host: string; port: number; debug: boolean } |
| 型のキーを取り出す | keyof User |
"id" | "name" | "email" |
| 値のキーを取り出す | keyof typeof config |
"host" | "port" | "debug" |
| 1つのプロパティの型 | User["name"] |
string |
| 複数のプロパティの型 | User["id" | "name"] |
string | number |
| すべての値の型(型から) | User[keyof User] |
string | number | undefined |
| すべての値の型(値から) | (typeof config)[keyof typeof config] |
string | number | boolean |
| 定数オブジェクトのキー | keyof typeof STATUS |
"draft" | "published" | "archived" |
| 定数オブジェクトの値 | (typeof STATUS)[keyof typeof STATUS] |
"下書き" | "公開中" | "保管" |
| 定数オブジェクトの1つの値 | (typeof STATUS)["draft"] |
"下書き" |
| 配列の要素の型(as const) | (typeof roles)[number] |
"admin" | "member" | "guest" |
| 配列の先頭の型 | (typeof roles)[0] |
"admin" |
| 配列の長さの型 | (typeof roles)["length"] |
3 |
| オブジェクト配列の要素の型 | (typeof users)[number] |
{ id: number; name: string } |
| オブジェクト配列の1項目の型 | (typeof users)[number]["name"] |
string |
| 省略できるプロパティの型 | User["email"] |
string | undefined |
| 一部のキーを除く | Exclude<keyof User, "id"> |
"name" | "email" |
覚え方は「値から始めるなら typeof、キーが欲しいなら keyof、値の型が欲しいなら [ ]」です。
| 記号 | 入力 | 出力 | ひとことで |
|---|---|---|---|
typeof 値 |
値(変数・定数・関数) | 型 | 値を型の世界に持ってくる |
keyof 型 |
型 | キーのユニオン型 | プロパティ名を取り出す |
型[キーの型] |
型とキー | 値の型 | プロパティの型を取り出す(インデックスアクセス型) |
型[number] |
配列・タプルの型 | 要素の型 | 配列の中身の型を取り出す |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript keyof typeofの使い方|キーと値の型を作る」
検証した環境と、型の確かめ方
| 項目 | 内容 |
|---|---|
| TypeScript | 7.0.2(npm i typescript で入った版) |
| Node.js | v22.22.2 |
| 型チェック | npx tsc --noEmit --strict --target es2022 ファイル名.ts |
| 実行 | node ファイル名.ts |
早見表の「得られる型」は、わざと never 型の変数に代入し、tsc のエラー文に表示された型を読み取って確かめています。
declare function show<T>(): T;
const x: never = show<keyof typeof config>();
error TS2322: Type '"debug" | "host" | "port"' is not assignable to type 'never'.
TypeScript 7.0.2 のエラー文では、ユニオン型の並びがアルファベット順("debug" | "host" | "port")で表示されました。ユニオン型は並び順に意味がないので、この記事の表では読みやすいよう定義の順に書いています。手元で型を確かめたいときは、エディタで型名にカーソルを合わせる方法も手軽です。
keyofの使い方(型からキーを取り出す)
keyof は、オブジェクトの型からプロパティ名のユニオン型を作ります。
type User = { id: number; name: string; email?: string };
type UserKey = keyof User; // "id" | "name" | "email"
const key1: UserKey = "name"; // OK
const key2: UserKey = "age"; // エラー
型の形によって、keyof の結果は次のように変わります(すべてエラー文で確かめた結果です)。
| 入力の型 | keyof の結果 |
補足 |
|---|---|---|
{ id: number; name: string; email?: string } |
"id" | "name" | "email" |
省略できるプロパティも含まれる |
{ [key: string]: number }(インデックスシグネチャ) |
string | number |
下で説明 |
Record<"a" | "b", number> |
"a" | "b" |
|
A | B(ユニオン型) |
両方に共通するキーだけ | A と B に共通する "id" だけになった |
A & B(インターセクション型) |
両方のキーすべて | "a" | "b" | "id" になった |
any |
string | number | symbol |
キーになれる型すべて |
(A は { id: number; a: string }、B は { id: number; b: string } で試しました。)
{ [key: string]: number } の keyof が string | number になるのは、JavaScript ではオブジェクトのキーがいつも文字列に変換されるためです。obj[0] と obj["0"] は同じプロパティを指すので、数値でのアクセスも認められます(公式ハンドブックの Keyof Type Operator のページを今回開いて確認しました)。
typeofの使い方(値から型を取り出す)
型を書く位置の typeof は、変数や関数から型を取り出します。 JavaScript に元からある typeof(実行時に "string" などの文字列を返す演算子)とは、書く場所で区別されます。
// typeof_misc.ts
// 関数から: 引数・戻り値の型を取り出す
function createUser(name: string) {
return { id: 1, name, tags: ["new"] };
}
type CreateUser = typeof createUser; // (name: string) => { id: number; name: string; tags: string[] }
type User = ReturnType<typeof createUser>; // { id: number; name: string; tags: string[] }
type UserKey = keyof User; // "id" | "name" | "tags"
type Tag = User["tags"][number]; // string
// クラスから: インスタンス側とクラス側でキーが違う
class Item {
static count = 0;
name = "";
size() {
return this.name.length;
}
}
type ItemKey = keyof Item; // "name" | "size"
type ItemStaticKey = keyof typeof Item; // "prototype" | "count"
// 実行時の typeof(値を調べて文字列を返す)と、型の位置の typeof(型を取り出す)は別物
const value: unknown = "abc";
console.log(typeof value);
const key: UserKey = "tags";
const staticKey: ItemStaticKey = "count";
console.log(key, staticKey, createUser("佐藤"));
実行結果:
string
tags count { id: 1, name: '佐藤', tags: [ 'new' ] }
| 書く場所 | 例 | 意味 |
|---|---|---|
| 式の中(実行時) | if (typeof value === "string") |
値の種類を文字列で返す。JavaScript の機能 |
| 型の位置 | type T = typeof config |
変数の型を取り出す。TypeScript の機能 |
typeof の入力になるものを整理します。
| 入力 | typeof で得られる型 |
よく続けて書くもの |
|---|---|---|
| オブジェクトの変数 | そのオブジェクトの型 | keyof typeof obj |
| 配列の変数 | 配列の型 | (typeof arr)[number] |
| 関数 | 関数の型 | ReturnType<typeof fn>、Parameters<typeof fn> |
| クラス | クラスそのもの(コンストラクタ)の型 | keyof typeof Item は static なメンバー名 |
| enum | enum オブジェクトの型 | keyof typeof Color はメンバー名("Red" | "Green") |
typeof を忘れて値をそのまま型の位置に書くと、TS2749 になります。 エラー文が直し方まで教えてくれます。
// err_basic.ts
const config = { host: "localhost", port: 5432 };
type User = { id: number; name: string };
type K1 = keyof config;
type V1 = config["host"];
type V2 = User["age"];
type V3 = User.name;
const key: keyof typeof config = "user";
function getValue(key: string) {
return config[key];
}
export {};
err_basic.ts(4,17): error TS2749: 'config' refers to a value, but is being used as a type here. Did you mean 'typeof config'?
err_basic.ts(5,11): error TS2749: 'config' refers to a value, but is being used as a type here. Did you mean 'typeof config'?
err_basic.ts(6,16): error TS2339: Property 'age' does not exist on type 'User'.
err_basic.ts(7,11): error TS2713: Cannot access 'User.name' because 'User' is a type, but not a namespace. Did you mean to retrieve the type of the property 'name' in 'User' with 'User["name"]'?
err_basic.ts(8,7): error TS2322: Type '"user"' is not assignable to type '"host" | "port"'.
err_basic.ts(10,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ host: string; port: number; }'.
No index signature with a parameter of type 'string' was found on type '{ host: string; port: number; }'.
| コード | 原因 | 直し方 |
|---|---|---|
| TS2749 | 値を型として使った | keyof typeof config、(typeof config)["host"] |
| TS2339 | 型にないキーで値の型を取り出そうとした | 存在するキーにする |
| TS2713 | 型にドットでアクセスした | User["name"] と角かっこで書く |
| TS2322 | キーのユニオン型にない文字列を代入した | "host" か "port" にする |
| TS7053 | ただの string でオブジェクトを引いた | 引数を key: keyof typeof config にする |
keyof typeofでオブジェクトのキーを型にする
keyof typeof は「typeof 値」を先に計算し、その結果に keyof を掛けます。
const config = { host: "localhost", port: 5432, debug: true };
// 手順1: typeof config → { host: string; port: number; debug: boolean }
// 手順2: keyof { host: string; port: number; debug: boolean } → "host" | "port" | "debug"
type ConfigKey = keyof typeof config;
function getConfig(key: ConfigKey) {
return config[key];
}
getConfig("port"); // OK
getConfig("user"); // エラー: "user" は ConfigKey にない
この書き方の良さは、オブジェクトにプロパティを足すと、キーの型も自動で増えることです。type ConfigKey = "host" | "port" | "debug" と手で書いた場合は、プロパティを足すたびに型も直す必要があります。
配列に keyof typeof を使うと、要素ではなく配列のメソッド名("length"、"push" など)が対象になります。配列の中身の型が欲しいときは、keyof ではなく (typeof arr)[number] を使います。
インデックスアクセス型 T[K] で値の型を取り出す
型の後ろに角かっこでキーの型を書くと、そのプロパティの型が得られます。 これをインデックスアクセス型と呼びます。
type User = { id: number; name: string; email?: string };
type Name = User["name"]; // string
type IdOrName = User["id" | "name"]; // string | number
type Values = User[keyof User]; // string | number | undefined
| 角かっこに入れるもの | 例 | 結果 |
|---|---|---|
| 1つのキー | User["name"] |
そのプロパティの型 |
| キーのユニオン | User["id" | "name"] |
それぞれの型のユニオン |
keyof T |
User[keyof User] |
すべての値の型のユニオン |
number |
string[][number]、(typeof roles)[number] |
配列・タプルの要素の型 |
| 数値リテラル | (typeof roles)[0] |
タプルのその位置の型 |
| つなげて書く | (typeof users)[number]["name"] |
入れ子の中の型 |
(typeof roles)[number] の number は「数値のインデックスでアクセスしたときの型」という意味です。配列はどの位置でも同じ要素の型を返すので、結果として要素の型のユニオンが得られます。typeof roles[number] とかっこなしで書いても同じ意味になりますが、(typeof roles)[number] とかっこで囲むと「先に typeof、次に [number]」という順番が読み取りやすくなります。
角かっこの中に書けるのは「型」だけです。値の変数を入れると次のエラーになります。
// err_key.ts
type User = { id: number; name: string };
const key = "name";
type V1 = User[key];
type V2 = User[typeof key];
let key2 = "name";
type V3 = User[typeof key2];
export {};
err_key.ts(3,16): error TS2538: Type 'key' cannot be used as an index type.
err_key.ts(3,16): error TS2749: 'key' refers to a value, but is being used as a type here. Did you mean 'typeof key'?
err_key.ts(6,16): error TS2537: Type 'User' has no matching index signature for type 'string'.
| 書き方 | 結果 | 理由 |
|---|---|---|
User[key] |
TS2538・TS2749 | key は値。型ではない |
User[typeof key](key は const) |
通る | const の文字列は "name" というリテラル型になる |
User[typeof key2](key2 は let) |
TS2537 | let の文字列は string 型に広がる |
as constの有無で値の型はどう変わるか
as const を付けないと、値の型は string に広がります。キーの型は変わりません。 同じ内容のオブジェクトと配列で比べました。
// asconst.ts
// as const なし
export const PLAIN = { draft: "下書き", published: "公開中" };
export type PlainKey = keyof typeof PLAIN;
export type PlainValue = (typeof PLAIN)[keyof typeof PLAIN];
export const plainList = ["admin", "member"];
export type PlainItem = (typeof plainList)[number];
// as const あり
export const CONST = { draft: "下書き", published: "公開中" } as const;
export type ConstKey = keyof typeof CONST;
export type ConstValue = (typeof CONST)[keyof typeof CONST];
export const constList = ["admin", "member"] as const;
export type ConstItem = (typeof constList)[number];
.d.ts に出力された変数の型:
export declare const PLAIN: {
draft: string;
published: string;
};
export declare const plainList: string[];
export declare const CONST: {
readonly draft: "下書き";
readonly published: "公開中";
};
export declare const constList: readonly ["admin", "member"];
| 取り出す型 | as const なし | as const あり |
|---|---|---|
| オブジェクトの型 | { draft: string; published: string } |
{ readonly draft: "下書き"; readonly published: "公開中" } |
キー(keyof typeof) |
"draft" | "published" |
"draft" | "published" |
値((typeof X)[keyof typeof X]) |
string |
"下書き" | "公開中" |
| 配列の型 | string[] |
readonly ["admin", "member"] |
配列の要素((typeof X)[number]) |
string |
"admin" | "member" |
キーだけが欲しいなら as const は不要です。値や配列の要素をユニオン型にしたいときは as const が必要になります。 「値のユニオン型を作ったはずなのに string になっている」ときは、まず as const の付け忘れを確かめます。
enumの代わりになる定数オブジェクトを作る
as const のオブジェクトと keyof typeof を組み合わせると、enum と同じように「決まった値の集まり」を表せます。
// enumlike.ts
// 1. 定数オブジェクトを as const で作る
export const OrderStatus = {
Pending: "pending",
Shipped: "shipped",
Done: "done",
} as const;
// 2. 同じ名前で「値のユニオン型」を作る(値と型は別の名前空間なので同名にできる)
export type OrderStatus = (typeof OrderStatus)[keyof typeof OrderStatus];
// 3. キーのユニオンが欲しいとき
export type OrderStatusKey = keyof typeof OrderStatus;
// 値 → 表示名の対応表。キーが足りないと型エラーになる
const LABEL: Record<OrderStatus, string> = {
pending: "受付済み",
shipped: "発送済み",
done: "完了",
};
function show(status: OrderStatus): string {
return LABEL[status];
}
console.log(show(OrderStatus.Shipped)); // 定数経由
console.log(show("done")); // 文字列リテラルを直接
console.log(Object.values(OrderStatus));
実行結果:
発送済み
完了
[ 'pending', 'shipped', 'done' ]
ここで作った型は次のとおりです。
| 型 | 中身 |
|---|---|
OrderStatus(値のユニオン) |
"pending" | "shipped" | "done" |
OrderStatusKey(キーのユニオン) |
"Pending" | "Shipped" | "Done" |
型のチェックがどう働くかも確かめます。
// err_enumlike.ts
const OrderStatus = { Pending: "pending", Shipped: "shipped", Done: "done" } as const;
type OrderStatus = (typeof OrderStatus)[keyof typeof OrderStatus];
const LABEL: Record<OrderStatus, string> = {
pending: "受付済み",
shipped: "発送済み",
};
function show(status: OrderStatus): string {
return status;
}
show("cancelled");
const fromApi: string = "done";
show(fromApi);
OrderStatus.Done = "finished";
export {};
err_enumlike.ts(4,7): error TS2741: Property 'done' is missing in type '{ pending: string; shipped: string; }' but required in type 'Record<OrderStatus, string>'.
err_enumlike.ts(11,6): error TS2345: Argument of type '"cancelled"' is not assignable to parameter of type 'OrderStatus'.
err_enumlike.ts(13,6): error TS2345: Argument of type 'string' is not assignable to parameter of type 'OrderStatus'.
err_enumlike.ts(14,13): error TS2540: Cannot assign to 'Done' because it is a read-only property.
| 行 | 守られること |
|---|---|
| 4行目 | 対応表に書き足し忘れた値(done)が、名前つきで示される |
| 11行目 | 候補にない文字列は渡せない |
| 13行目 | ただの string(外から来た値など)は、確かめてからでないと渡せない |
| 14行目 | 定数オブジェクトは書き換えられない |
enum と比べた特徴は次のとおりです。
| 観点 | 定数オブジェクト + as const | enum |
|---|---|---|
| 実行時の姿 | ただのオブジェクト | enum 用に生成されたオブジェクト |
| 文字列リテラルを直接渡す | できる(show("done")) |
文字列 enum ではできない(Status.Done と書く) |
| 値の一覧を取る | Object.values(OrderStatus) |
Object.values(Status)(数値 enum は逆引きの分も含まれる) |
| キーの型 | keyof typeof OrderStatus |
keyof typeof Status |
どちらも用途に合えば使えます。なお、enum を含む .ts ファイルを Node.js v22.22.2 で直接 node ファイル名.ts と実行したところ、ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX: TypeScript enum is not supported in strip-only mode で止まりました。型を取り除くだけの実行方法では enum の構文を変換できないためです。定数オブジェクトはただの JavaScript なので、この実行方法でもそのまま動きます。
Object.keysがstring[]になる理由と型安全な回避コード
Object.keys の戻り値は、どんなオブジェクトを渡しても string[] です。 そのため、取り出したキーで元のオブジェクトを引くと TS7053 になります。
// err_objectkeys.ts
const stock = { apple: 3, orange: 0, grape: 12 };
for (const key of Object.keys(stock)) {
console.log(stock[key]);
}
for (const key in stock) {
console.log(stock[key]);
}
export {};
err_objectkeys.ts(3,15): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ apple: number; orange: number; grape: number; }'.
No index signature with a parameter of type 'string' was found on type '{ apple: number; orange: number; grape: number; }'.
err_objectkeys.ts(6,15): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ apple: number; orange: number; grape: number; }'.
No index signature with a parameter of type 'string' was found on type '{ apple: number; orange: number; grape: number; }'.
for...in でも同じエラーになります。
なぜ (keyof T)[] ではないのか
理由は、TypeScript のオブジェクトの型は「少なくともこのプロパティを持つ」という意味で、ほかのプロパティを持っていてもよいからです。次のコードで確かめられます。
// why_string.ts
type Point = { x: number; y: number };
function printAll(p: Point) {
for (const key of Object.keys(p)) console.log(key);
}
// 型に書いていないプロパティを持つオブジェクトも、変数経由なら Point として渡せる
const p3 = { x: 1, y: 2, label: "A" };
printAll(p3);
実行結果(型チェックも通ります):
x
y
label
p の型は Point なのに、実行すると label というキーも出てきました。もし Object.keys の戻り値が ("x" | "y")[] だったら、型と実際が食い違います。string[] という戻り値は、この食い違いを起こさないための控えめな型です。
回避コード4つ
// objectkeys.ts
const stock = { apple: 3, orange: 0, grape: 12 };
// 回避1: 型アサーションでキーのユニオンにする(オブジェクトの出どころが自分のコードのとき)
for (const key of Object.keys(stock) as (keyof typeof stock)[]) {
console.log("1:", key, stock[key]);
}
// 回避2: 使い回せる関数にする
function typedKeys<T extends object>(obj: T): (keyof T)[] {
return Object.keys(obj) as (keyof T)[];
}
export const keys = typedKeys(stock);
console.log("2:", keys);
// 回避3: Object.entries なら値も一緒に取れる(キーは string のまま、値に型が付く)
for (const [key, count] of Object.entries(stock)) {
console.log("3:", key, count + 1);
}
// 回避4: 外から来た文字列は、型ガードで確かめてから使う
function isKey<T extends object>(obj: T, key: PropertyKey): key is keyof T {
return Object.hasOwn(obj, key);
}
const input: string = "grape";
if (isKey(stock, input)) {
console.log("4:", input, stock[input]);
}
console.log("4:", isKey(stock, "melon"));
実行結果:
1: apple 3
1: orange 0
1: grape 12
2: [ 'apple', 'orange', 'grape' ]
3: apple 4
3: orange 1
3: grape 13
4: grape 12
4: false
typedKeys(stock) の型は ("apple" | "grape" | "orange")[] と出力されました。for...in を使いたい場合は、let key: keyof typeof stock; を先に宣言してから for (key in stock) と書く方法でも通ります(こちらも実行して確認しました)。
| 回避方法 | 向いている場面 | 気をつける点 |
|---|---|---|
1. as (keyof typeof obj)[] |
その場で定義した定数オブジェクトを回す | 型にないプロパティが混ざらないと言い切れるときに使う |
2. typedKeys 関数 |
同じ処理が何か所にもある | 中身は1と同じ型アサーション。使う条件も同じ |
3. Object.entries |
キーと値の両方を使う | キーは string のまま。値には型が付く |
4. 型ガード isKey |
入力値や外部から届いた文字列でオブジェクトを引く | 実行時にも確かめるので、型と実際が食い違わない |
型アサーション(1と2)は「型にないキーは入っていない」と自分で請け負う書き方です。 自分で as const で定義した定数オブジェクトなら問題ありません。外部から受け取ったオブジェクトや、引数として渡ってきたオブジェクトには、3か4を選ぶと安心です。
satisfiesと併用して、形を検査しながらキーを取り出す
as const satisfies 型 と書くと、「形が合っているかの検査」と「リテラル型のままキーを取り出すこと」を両立できます。
// satisfies.ts
type Route = { path: string; auth: boolean };
// as const satisfies: 値はリテラル型のまま、形だけ Route に合っているか検査する
export const ROUTES = {
home: { path: "/", auth: false },
mypage: { path: "/mypage", auth: true },
} as const satisfies Record<string, Route>;
export type RouteName = keyof typeof ROUTES; // "home" | "mypage"
export type RoutePath = (typeof ROUTES)[RouteName]["path"]; // "/" | "/mypage"
// 型注釈にした場合(比較用): キーが string に広がる
export const ROUTES2: Record<string, Route> = {
home: { path: "/", auth: false },
mypage: { path: "/mypage", auth: true },
};
export type RouteName2 = keyof typeof ROUTES2; // string
function go(name: RouteName): string {
return ROUTES[name].path;
}
console.log(go("mypage"));
実行結果:
/mypage
.d.ts に出力された型を比べます。
export declare const ROUTES: {
readonly home: {
readonly path: "/";
readonly auth: false;
};
readonly mypage: {
readonly path: "/mypage";
readonly auth: true;
};
};
export declare const ROUTES2: Record<string, Route>;
| 書き方 | 形の検査 | keyof typeof の結果 |
値の型 |
|---|---|---|---|
const X = {...} |
なし | キーのユニオン | string などに広がる |
const X = {...} as const |
なし | キーのユニオン | リテラル型 |
const X: Record<string, Route> = {...} |
あり | string(キーの情報がなくなる) |
Route |
const X = {...} satisfies Record<string, Route> |
あり | キーのユニオン | string などに広がる |
const X = {...} as const satisfies Record<string, Route> |
あり | キーのユニオン | リテラル型 |
型注釈(: Record<string, Route>)を付けると、変数の型は注釈した型そのものになり、keyof typeof で個々のキーを取り出せなくなります。 satisfies は検査だけをして、変数の型は推論されたまま残します。
形が合っていないときのエラーも確かめました。
// err_satisfies.ts
type Route = { path: string; auth: boolean };
const ROUTES = {
home: { path: "/", auth: false },
mypage: { path: "/mypage", auth: "yes" },
help: { path: "/help" },
} as const satisfies Record<string, Route>;
const OK = {
home: { path: "/", auth: false },
} as const satisfies Record<string, Route>;
type RouteName = keyof typeof OK;
const n: RouteName = "settings";
export {};
err_satisfies.ts(4,30): error TS2322: Type 'string' is not assignable to type 'boolean'.
err_satisfies.ts(5,3): error TS2741: Property 'auth' is missing in type '{ readonly path: "/help"; }' but required in type 'Route'.
err_satisfies.ts(12,7): error TS2322: Type '"settings"' is not assignable to type '"home"'.
型の不一致(4行目)、プロパティの不足(5行目)が定義の場所で見つかり、キーの型(12行目)も "home" だけに絞られています。
ジェネリクスと組み合わせる(K extends keyof T)
関数の引数で「このオブジェクトのキーのどれか」を受け取りたいときは、K extends keyof T と書きます。 戻り値を T[K] にすると、渡したキーに応じて戻り値の型が変わります。
// generic.ts
// K extends keyof T: 「T のキーのどれか」だけを受け取り、T[K] でその値の型を返す
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
function setProp<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value;
}
function pluck<T, K extends keyof T>(items: readonly T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K> {
const out = {} as Pick<T, K>;
for (const k of keys) out[k] = obj[k];
return out;
}
const user = { id: 1, name: "佐藤", active: true };
export const name = getProp(user, "name"); // string
export const active = getProp(user, "active"); // boolean
setProp(user, "name", "鈴木");
export const ids = pluck([user, { id: 2, name: "田中", active: false }], "id"); // number[]
export const picked = pick(user, "id", "name");
console.log(name, active, user.name, ids, picked);
実行結果:
佐藤 true 鈴木 [ 1, 2 ] { id: 1, name: '鈴木' }
getProp(user, "name") は string、getProp(user, "active") は boolean と、キーごとに戻り値の型が切り替わります(.d.ts の出力で確認)。合わない呼び出しは次のように止まります。
// err_generic.ts(呼び出し部分)
getProp(user, "email");
setProp(user, "id", "1");
function bad<T>(obj: T, key: string) {
return obj[key];
}
err_generic.ts(8,15): error TS2345: Argument of type '"email"' is not assignable to parameter of type '"active" | "id" | "name"'.
err_generic.ts(9,21): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
err_generic.ts(11,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'unknown'.
No index signature with a parameter of type 'string' was found on type 'unknown'.
| 行 | 読み取れること |
|---|---|
| 8行目 | ないキーを渡すと、使えるキーの一覧がエラー文に出る |
| 9行目 | setProp は、キー "id" に対応する値の型(number)まで検査する |
| 11行目 | key を string にすると、関数の中で obj[key] が書けない。K extends keyof T が必要な理由 |
keyofでマップ型を作る
keyof は、型のキーを順に回して新しい型を作る「マップ型」でも使います。
const user = { id: 1, name: "佐藤", active: true };
type User = typeof user;
// すべてのプロパティを boolean にした型
type Flags = { [K in keyof User]: boolean };
// キーの名前を変えた型(テンプレートリテラル型と組み合わせる)
type Getters = { [K in keyof User as `get${Capitalize<K>}`]: () => User[K] };
エラー文で確かめた結果は次のとおりです。
| 型 | 得られた型 |
|---|---|
Flags |
{ id: boolean; name: boolean; active: boolean } |
Getters |
{ getId: () => number; getName: () => string; getActive: () => boolean } |
`on${Capitalize<keyof typeof STATUS>}` |
"onDraft" | "onPublished" | "onArchived" |
Partial<T> や Pick<T, K> といった組み込みのユーティリティ型も、内部ではこの [K in keyof T] の形で作られています。
やりたいことから選ぶ逆引き表
| やりたいこと | 書き方 |
|---|---|
| 定数オブジェクトのキーを引数の型にする | key: keyof typeof OBJ |
| 定数オブジェクトの値をユニオン型にする | (typeof OBJ)[keyof typeof OBJ](OBJ は as const) |
| 定数配列の要素をユニオン型にする | (typeof LIST)[number](LIST は as const) |
| オブジェクト配列の1項目をユニオン型にする | (typeof LIST)[number]["id"](LIST は as const) |
| 型の一部のプロパティの型を再利用する | User["name"] |
| 関数の戻り値のキーを取り出す | keyof ReturnType<typeof fn> |
| キーに応じて値の型が変わる関数を書く | <T, K extends keyof T>(obj: T, key: K): T[K] |
| 値 → 表示名の対応表を、書き足し忘れなく作る | Record<(typeof OBJ)[keyof typeof OBJ], string> |
| Object.keys の結果でオブジェクトを引く | Object.keys(obj) as (keyof typeof obj)[]、または型ガード |
| 形を検査しつつキーを取り出す | as const satisfies Record<string, 型> と keyof typeof |
keyof typeofのチェックリスト
| 確認すること | 目安 |
|---|---|
| 値に keyof を直接掛けていないか | keyof typeof 値 と書く(TS2749) |
| 値のユニオン型が string になっていないか | as const を付ける |
| 配列に keyof を使っていないか | 要素の型は (typeof arr)[number] |
| 型注釈でキーの情報をなくしていないか | : Record<string, T> ではなく satisfies |
| 角かっこの中に値の変数を書いていないか | typeof key、または型エイリアスにする |
| Object.keys の型アサーションは、自分で定義したオブジェクトに限っているか | 外から来る値は型ガードで確かめる |
| 関数の引数のキーを string にしていないか | K extends keyof T |
よくある質問
Q. keyof typeof は、keyof と typeof のどちらが先に働きますか?
A. typeof が先です。keyof typeof config は keyof (typeof config) と同じ意味で、まず値から型を取り出し、その型のキーを取り出します。
Q. (typeof arr)[number] の number は何を表していますか?
A. 「数値のインデックスでアクセスしたときに得られる型」を表します。配列やタプルはどの位置を読んでも要素の型が返るので、結果は要素の型のユニオンになります。as const を付けた配列なら、"admin" | "member" のようなリテラルのユニオン型が得られます。
Q. Object.keys の結果を keyof の型にしてよいのはどんなときですか? A. オブジェクトを自分のコードの中で定義していて、型に書いたプロパティしか持たないと言い切れるときです。関数の引数や外部から届いたオブジェクトは、型にないプロパティを持つことがあるので、Object.entries を使うか、型ガードで確かめる書き方が合います。
Q. as const と satisfies はどちらを先に書きますか?
A. as const satisfies 型 の順です。as const でリテラル型にしたうえで、その値が指定した型に合うかを satisfies が検査します。
まとめ
- typeof は値から型、keyof は型からキー、T[K] は型から値の型を取り出す。値のキーが欲しいときは
keyof typeof 値 - 値のユニオン型は
(typeof OBJ)[keyof typeof OBJ]、配列の要素のユニオン型は(typeof LIST)[number] - as const がないと値の型は string に広がる。キーの型は as const なしでも取り出せる
- as const のオブジェクトと同名の型を組み合わせると、決まった値の集まりを表せる。対応表は
Record<値の型, string>にすると書き足し忘れが型エラーになる - Object.keys が string[] なのは、オブジェクトが型にないプロパティを持てるから。自分で定義した定数は型アサーション、外から来る値は型ガードで扱う
as const satisfies 型で、形の検査とキーの取り出しを両立できる。型注釈にするとキーが string に広がる- 関数では
K extends keyof TとT[K]で、キーに応じた値の型を返せる
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript keyof typeofの使い方|キーと値の型を作る」
株式会社RJCでは、TypeScriptのkeyofとtypeofによる型の作り方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。