TypeScript上級講座 #1 keyofとtypeof
TypeScript基礎講座と+ React 実践を終えたなら、いよいよ一段階上 — 型を直接加工する道具たちを扱う番です。このシリーズは7編で構成されます。
- #1 keyofとtypeof ← 今回
- #2 Mapped types
- #3 Conditional typesとinfer
- #4 Template literal types
- #5 Discriminated unionと型ガード深掘り
- #6 モジュールと.d.ts
- #7 実戦パターンとアンチパターン
基礎講座が「型をどう書くか」だったなら、このシリーズは「型をどう計算するか」です。最初の道具は最も基本の二つ — keyofとtypeofです。
keyof — オブジェクト型のキーをunionに集める
#
keyof Tはオブジェクト型Tのすべてのキーを文字列リテラルunionとして作ってくれます。
type User = {
id: string;
name: string;
age: number;
};
type UserKey = keyof User;
// 'id' | 'name' | 'age'
最初に見るとたいしたことないように思えますが、これがあって初めて「Tのキーのうち一つ」という表現が可能になります。最もよくある活用はインデックスシグネチャを安全に扱う関数です。
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { id: 'u1', name: 'カーティス', age: 30 };
getValue(user, 'name'); // string
getValue(user, 'age'); // number
getValue(user, 'unknown'); // ✗ 'unknown'はkeyof Userではない
三行の関数ですが、二つを同時に保証します。
- 存在しないキーは呼び出し段階でブロック
- 戻り値の型がキーに応じて異なる形で絞り込まれる —
nameならstring、ageならnumber
T[K]はインデックスアクセス型です。「Tという型からキーKで取り出した値の型」を意味します。JavaScriptのobj[key]と形が同じで直観的です。
インデックスシグネチャ vs 明示キー #
keyofの結果はオブジェクト定義の方法によって変わります。
type A = { id: string; name: string };
type AK = keyof A; // 'id' | 'name' (狭いunion)
type B = { [key: string]: string };
type BK = keyof B; // string | number (numberはJavaScriptがキーを自動変換するため)
明示的に書いたキーだけ狭いリテラルとして取られます。インデックスシグネチャを使うとkeyofの精度が落ちます。可能ならオブジェクトの形を明示的に書く方が、型安全の観点で有利です。
数値キーもunionに入る — 配列のkeyof #
配列型のkeyofはインデックス(数値)とメソッド名のすべてを含みます。
type Arr = string[];
type ArrK = keyof Arr;
// number | 'length' | 'toString' | 'push' | ...
配列にkeyofを直接使うことはほぼありません。T[number]で要素型を取り出すパターンの方がよく使われます。
type Arr = { id: string; name: string }[];
type Item = Arr[number]; // { id: string; name: string }
numberは「任意の数値インデックスでアクセスしたときの型」を意味します。タプルではない一般的な配列で最も短い要素-型抽出パターンです。
typeof — 値から型を引き出す
#
TypeScriptのtypeofはJavaScriptのtypeofと名前は同じですが違うところで使われます。
- JavaScript: 値を表現するところ —
typeof x === 'string'(値の段階) - TypeScript: 型を表現するところ —
let y: typeof x(型の段階)
const config = {
baseUrl: 'https://api.example.com',
timeout: 5000,
};
type Config = typeof config;
// { baseUrl: string; timeout: number }
すでに書いてある値から型を引き出します。値を一度だけ書いて、型はそこから自動で追従させるのが核心の効用です。同じ情報を二箇所に書く重複を無くしてくれます。
関数にも、importにも #
typeofはどんな値にも使えます。
function createUser(name: string, age: number) {
return { id: crypto.randomUUID(), name, age };
}
type CreateUser = typeof createUser;
// (name: string, age: number) => { id: string; name: string; age: number }
type User = ReturnType<typeof createUser>;
// { id: string; name: string; age: number }
ReturnType<typeof fn>パターンは本当によく使います。「この関数が返すオブジェクトの型」を別途書かずに引き出せます。ReturnTypeの定義は#3で直接作ってみます。
import * as utils from './utils';
type Utils = typeof utils; // モジュール全体の型
keyof typeof — 最もよく使う組み合わせ
#
keyofは型を受け取り、typeofは値から型を作ります。二つを組み合わせると値として定義されたオブジェクトのキーをunionとして引き出すことができます。
const STATUS = {
idle: 'idle',
loading: 'loading',
done: 'done',
error: 'error',
} as const;
type StatusKey = keyof typeof STATUS;
// 'idle' | 'loading' | 'done' | 'error'
type StatusValue = typeof STATUS[StatusKey];
// 'idle' | 'loading' | 'done' | 'error'
JavaScriptでenum-likeなオブジェクトを定義し、そこからunion型を自動的に作り出すのが要点です。オブジェクトにキーを追加するとunionも自動で増えます。単一の出典 (single source of truth)を作るパターンなので、非常に価値があります。
as constが要点の鍵
#
as constが無いと、同じコードが次のように崩れます。
const STATUS = {
idle: 'idle',
loading: 'loading',
};
type StatusValue = typeof STATUS[keyof typeof STATUS];
// string ← 広すぎる
as constがあってこそTypeScriptが各値をリテラル型に絞り込みます。オブジェクトや配列を「この形そのまま、この値そのまま固定」しろという信号です。as constが無いとすべてが単にstring、numberに広がってしまいます。
as constの効果を正確に表で見ると:
const a = ['red', 'green', 'blue'];
// ^ string[]
const b = ['red', 'green', 'blue'] as const;
// ^ readonly ['red', 'green', 'blue']
const c = { mode: 'dark' };
// ^ { mode: string }
const d = { mode: 'dark' } as const;
// ^ { readonly mode: 'dark' }
as constを付けると型はその値そのままになります。配列はreadonlyタプルに、オブジェクトはすべてのフィールドがreadonlyのリテラルに。「私が書いたそのまま固定」と覚えれば直観的です。
実戦1 — ルートマップからunionを作る #
APIルートやページルートをオブジェクトで定義し、そのキーを安全なunionとして使いたいとき。
const ROUTES = {
home: '/',
about: '/about',
blog: '/blog',
blogPost: '/blog/:slug',
} as const;
type RouteName = keyof typeof ROUTES;
// 'home' | 'about' | 'blog' | 'blogPost'
type RoutePath = typeof ROUTES[RouteName];
// '/' | '/about' | '/blog' | '/blog/:slug'
function navigate(name: RouteName) {
const path = ROUTES[name];
// ...
}
navigate('home'); // OK
navigate('contact'); // ✗ contactは定義されていない
ROUTESに新しいルートを追加するとRouteNameも自動で拡張されます。一箇所で定義 → 型自動同期。
実戦2 — actionのtypeを自動で集める #
reducerのactionをオブジェクトで定義し、そこからunionを自動生成するパターン。
const ActionTypes = {
ADD_TODO: 'todos/add',
REMOVE_TODO: 'todos/remove',
TOGGLE_TODO: 'todos/toggle',
} as const;
type ActionType = typeof ActionTypes[keyof typeof ActionTypes];
// 'todos/add' | 'todos/remove' | 'todos/toggle'
こうしておくとdispatch({ type: ActionTypes.ADD_TODO, ... })の形で呼ぶときに型が正確に合い、型定義を別途維持しなくて済みます。
keyofが光るところ — フォームフィールド検証
#
フォームをオブジェクトとして扱い、フィールド名を型で絞り込みたいとき。
type SignupForm = {
email: string;
password: string;
agree: boolean;
};
function setField<K extends keyof SignupForm>(
form: SignupForm,
key: K,
value: SignupForm[K]
): SignupForm {
return { ...form, [key]: value };
}
const form: SignupForm = { email: '', password: '', agree: false };
setField(form, 'email', 'me@example.com'); // OK
setField(form, 'agree', true); // OK
setField(form, 'agree', 'yes'); // ✗ agreeはboolean
setField(form, 'unknown', '...'); // ✗ unknownキーが無い
三行の関数に存在しないキーの遮断とキー別の値の型一致の二つが一度に入っています。JavaScriptではドキュメントやif文で防がねばならない領域です。
まとめ #
今回整理した内容:
keyof T— オブジェクト型のキーのunionT[K]— インデックスアクセス型、キーで値の型を取り出すT[number]— 配列の要素型抽出typeof value— 値から型を引き出す、型段階の表現as const— リテラル型をそのまま固定 (オブジェクト + 配列)keyof typeof OBJ— 値で定義したオブジェクトからキーunionを自動生成- 実戦 — ルートマップ、action型、フォームフィールド安全検証
この二つの道具に慣れると、次の道具たちの基盤が整います。次の記事(#2 Mapped types)ではkeyofの上に一段階さらに — オブジェクト型を丸ごと変換するmapped typesを扱います。Partial、Required、Readonlyのような組み込みユーティリティがどう作られたかも自分で書いてみながらです。