0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【TypeScript】ネストしたオブジェクトのキーをドット記法のパスとして型安全に扱う

0
Posted at

はじめに

user.preferences.theme」のようなドット区切りのパス文字列、実務で扱ったことはありませんか?

状態管理で特定のキーにアクセスしたり、フォームライブラリでフィールド名を指定したり、i18nのキーを渡したり——こうした場面でパス文字列はよく登場します。ただし、普通に string 型で扱ってしまうとタイポしても気づけません。

この記事では、ネストしたオブジェクトのすべてのキーをドット記法のUnion型として自動生成する Path<T> 型を紹介します。

本記事は @KaraBharat氏のツイート を参考に、実務での活用方法を補足してまとめたものです。

完成形

先に完成形を見てみましょう。

type Path<T> = T extends object
  ? {
      [K in keyof T]: K extends string ? `${K}` | `${K}.${Path<T[K]>}` : never;
    }[keyof T]
  : never;

これを使うと、こうなります。

interface AppState {
  user: {
    id: string;
    preferences: {
      theme: "light" | "dark";
      lang: "en" | "es";
    };
  };
  ui: {
    sidebarOpen: boolean;
  };
}

type AppStatePath = Path<AppState>;
// "user"
// | "ui"
// | "user.id"
// | "user.preferences"
// | "user.preferences.theme"
// | "user.preferences.lang"
// | "ui.sidebarOpen"

AppState のネスト構造をすべて辿って、ドット区切りのパスがUnion型として列挙されます。

型の仕組みを分解して理解する

一見すると複雑ですが、やっていることは3ステップです。

ステップ1: オブジェクトかどうかを判定

T extends object ? { ... } : never;

T がオブジェクト型でなければ never(=末端に到達)。stringboolean のようなプリミティブ型に対しては再帰しません。

ステップ2: 各キーに対してマッピング

[K in keyof T]: K extends string ? `${K}` | `${K}.${Path<T[K]>}` : never;

keyof T でオブジェクトの各キー K を取り出し、テンプレートリテラル型で2種類の文字列を生成します。

  • ${K} — そのキー自体(例: "user"
  • `${K}.${Path<T[K]>}` — そのキー+子のパスを再帰的に連結(例: "user.id", "user.preferences.theme"

K extends string のガードは、keyofstring | number | symbol を返す可能性があるため、文字列キーだけに限定するためのものです。

ステップ3: Mapped Typeの値をUnionに変換

}[keyof T]

最後の [keyof T]Indexed Access Type です。Mapped Typeで生成したオブジェクト型の全プロパティの値を取り出して、Union型にまとめます。

// イメージとしてはこう展開される
{
  user: "user" | "user.id" | "user.preferences" | "user.preferences.theme" | "user.preferences.lang";
  ui: "ui" | "ui.sidebarOpen";
}[keyof AppState]

// ↓ [keyof AppState] で値だけ取り出すと...

// "user" | "user.id" | "user.preferences" | "user.preferences.theme" | "user.preferences.lang"
// | "ui" | "ui.sidebarOpen"

実務での使いどころ

1. 型安全な状態アクセス関数

状態管理で特定のパスの値を取得・更新する関数に使えます。

function getByPath<T>(obj: T, path: Path<T>): unknown {
  return path.split(".").reduce((acc: any, key) => acc?.[key], obj);
}

const state: AppState = {
  user: { id: "1", preferences: { theme: "dark", lang: "en" } },
  ui: { sidebarOpen: false },
};

getByPath(state, "user.preferences.theme"); // ✅ OK
getByPath(state, "user.preferences.color"); // ❌ コンパイルエラー!

パスをタイポすると コンパイル時に エラーになります。

2. フォームライブラリのフィールド名

React Hook Form などでネストしたフォームのフィールド名を扱う場面で、入力補完が効くようになります。

function useFormField(name: Path<FormValues>) {
  // name は "user.name" | "user.email" | "address.city" | ... のようなUnion型
}

3. i18n / 翻訳キーの型安全化

翻訳ファイルのキーを型で縛ることで、存在しないキーへのアクセスを防げます。

const translations = {
  header: { title: "ようこそ", subtitle: "ログインしてください" },
  footer: { copyright: "© 2025" },
} as const;

type TranslationKey = Path<typeof translations>;
// "header" | "footer" | "header.title" | "header.subtitle" | "footer.copyright"

function t(key: TranslationKey): string { ... }

t("header.title");    // ✅
t("header.typo");     // ❌ コンパイルエラー

注意点

循環参照には対応していない

自分自身を参照するような型に Path<T> を適用すると、再帰が無限ループになりTypeScriptがエラーを出します。

interface TreeNode {
  value: string;
  children: TreeNode[]; // 循環参照
}

type TreePath = Path<TreeNode>; // ❌ Type instantiation is excessively deep

対策としては、再帰の深さを制限するパターンがあります。

type Path<T, Depth extends number[] = []> = Depth["length"] extends 5
  ? never // 5階層で打ち止め
  : T extends object
    ? {
        [K in keyof T]: K extends string
          ? `${K}` | `${K}.${Path<T[K], [...Depth, 0]>}`
          : never;
      }[keyof T]
    : never;

配列のインデックスは含まれない

この実装では配列のインデックス(items.0.name のような数値パス)は生成されません。配列要素へのパスが必要な場合は追加の工夫が必要です。

まとめ

Path<T> 型は、TypeScriptの以下のテクニックを組み合わせた応用例です。

  • Conditional Types (T extends object ? ... : never)
  • Mapped Types ([K in keyof T])
  • Template Literal Types (`${K}.${Path<T[K]>}`)
  • Indexed Access Types ({ ... }[keyof T])
  • 再帰型Path<T[K]> で自分自身を呼び出す)

ドット記法のパスを string で扱っている箇所があれば、この型を導入するだけでタイポや存在しないキーへのアクセスがコンパイル時に検出できるようになります。ぜひ試してみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?