TypeScript上級講座 #1 keyofとtypeof

読了 7分

TypeScript基礎講座+ React 実践を終えたなら、いよいよ一段階上 — 型を直接加工する道具たちを扱う番です。このシリーズは7編で構成されます。

  • #1 keyofとtypeof ← 今回
  • #2 Mapped types
  • #3 Conditional typesとinfer
  • #4 Template literal types
  • #5 Discriminated unionと型ガード深掘り
  • #6 モジュールと.d.ts
  • #7 実戦パターンとアンチパターン

基礎講座が「型をどう書くか」だったなら、このシリーズは「型をどう計算するか」です。最初の道具は最も基本の二つ — keyoftypeofです。

keyof — オブジェクト型のキーをunionに集める #

keyof Tはオブジェクト型Tのすべてのキーを文字列リテラルunionとして作ってくれます。

keyof基本
type User = {
  id: string;
  name: string;
  age: number;
};

type UserKey = keyof User;
// 'id' | 'name' | 'age'

最初に見るとたいしたことないように思えますが、これがあって初めて「Tのキーのうち一つ」という表現が可能になります。最もよくある活用はインデックスシグネチャを安全に扱う関数です。

安全なgetValue
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ではない

三行の関数ですが、二つを同時に保証します。

  1. 存在しないキーは呼び出し段階でブロック
  2. 戻り値の型がキーに応じて異なる形で絞り込まれる — nameならstringageならnumber

T[K]インデックスアクセス型です。「Tという型からキーKで取り出した値の型」を意味します。JavaScriptのobj[key]と形が同じで直観的です。

インデックスシグネチャ vs 明示キー #

keyofの結果はオブジェクト定義の方法によって変わります。

明示キー vs インデックスシグネチャ
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はインデックス(数値)とメソッド名のすべてを含みます。

keyof Array — ほぼ使わない
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 (型の段階)
typeof基本
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もtypeof
import * as utils from './utils';

type Utils = typeof utils;   // モジュール全体の型

keyof typeof — 最もよく使う組み合わせ #

keyofは型を受け取り、typeofは値から型を作ります。二つを組み合わせると値として定義されたオブジェクトのキーをunionとして引き出すことができます。

値からキー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が無いと、同じコードが次のように崩れます。

as constなし
const STATUS = {
  idle: 'idle',
  loading: 'loading',
};

type StatusValue = typeof STATUS[keyof typeof STATUS];
// string  ← 広すぎる

as constがあってこそTypeScriptが各値をリテラル型に絞り込みます。オブジェクトや配列を「この形そのまま、この値そのまま固定」しろという信号です。as constが無いとすべてが単にstringnumberに広がってしまいます。

as constの効果を正確に表で見ると:

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として使いたいとき。

route map
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を自動生成するパターン。

action型自動化
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 — オブジェクト型のキーのunion
  • T[K] — インデックスアクセス型、キーで値の型を取り出す
  • T[number] — 配列の要素型抽出
  • typeof value — 値から型を引き出す、型段階の表現
  • as const — リテラル型をそのまま固定 (オブジェクト + 配列)
  • keyof typeof OBJ — 値で定義したオブジェクトからキーunionを自動生成
  • 実戦 — ルートマップ、action型、フォームフィールド安全検証

この二つの道具に慣れると、次の道具たちの基盤が整います。次の記事(#2 Mapped types)ではkeyofの上に一段階さらに — オブジェクト型を丸ごと変換するmapped typesを扱います。PartialRequiredReadonlyのような組み込みユーティリティがどう作られたかも自分で書いてみながらです。

X