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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?