Typescriptで深層キーオブ:型安全なコードを実現する

2024-04-12

Typescriptでネストされたオブジェクトのディープキーオブを取得する方法はいくつかあります。この解説では、代表的な3つの方法とそのメリットとデメリットを紹介します。

方法

  1. keyof と typeof を組み合わせる
type DeepKeyOf<T> = {
  [K in keyof T]: K extends string | number ? K : DeepKeyOf<T[K]>;
};

type User = {
  name: string;
  age: number;
  address: {
    city: string;
    postalCode: number;
  };
};

type DeepKeys = DeepKeyOf<User>; // "name" | "age" | "address.city" | "address.postalCode"

メリット:

  • シンプルで分かりやすい
  • ライブラリ不要
  • ネストが深い場合、コードが冗長になる
  • 型エイリアスの再帰的な定義が必要
  1. Paths 型を使用する
import { Paths } from "ts-toolbelt";

type User = {
  name: string;
  age: number;
  address: {
    city: string;
    postalCode: number;
  };
};

type DeepKeys = Paths<User>; // "name" | "age" | "address.city" | "address.postalCode"

  • コードが簡潔になる
  • ライブラリのインストールが必要
  1. deep-keyof ライブラリを使用する
import { deepKeyOf } from "deep-keyof";

type User = {
  name: string;
  age: number;
  address: {
    city: string;
    postalCode: number;
  };
};

type DeepKeys = deepKeyOf<User>; // "name" | "age" | "address.city" | "address.postalCode"

ネストされたオブジェクトのディープキーオブを取得するには、いくつかの方法があります。それぞれメリットとデメリットがあるので、状況に合わせて最適な方法を選択してください。




keyof と typeof を組み合わせる

type DeepKeyOf<T> = {
  [K in keyof T]: K extends string | number ? K : DeepKeyOf<T[K]>;
};

type User = {
  name: string;
  age: number;
  address: {
    city: string;
    postalCode: number;
  };
};

type DeepKeys = DeepKeyOf<User>; // "name" | "age" | "address.city" | "address.postalCode"

const user: User = {
  name: "John Doe",
  age: 30,
  address: {
    city: "New York",
    postalCode: 10001,
  },
};

// DeepKeys 型を使用して、ユーザーの深いプロパティにアクセスできます
console.log(user.name); // "John Doe"
console.log(user.address.city); // "New York"
console.log(user["address.postalCode"]); // 10001

Paths 型を使用する

import { Paths } from "ts-toolbelt";

type User = {
  name: string;
  age: number;
  address: {
    city: string;
    postalCode: number;
  };
};

type DeepKeys = Paths<User>; // "name" | "age" | "address.city" | "address.postalCode"

const user: User = {
  name: "John Doe",
  age: 30,
  address: {
    city: "New York",
    postalCode: 10001,
  },
};

// Paths 型を使用して、ユーザーの深いプロパティにアクセスできます
console.log(user.name); // "John Doe"
console.log(user.address.city); // "New York"
console.log(user["address.postalCode"]); // 10001

deep-keyof ライブラリを使用する

import { deepKeyOf } from "deep-keyof";

type User = {
  name: string;
  age: number;
  address: {
    city: string;
    postalCode: number;
  };
};

type DeepKeys = deepKeyOf<User>; // "name" | "age" | "address.city" | "address.postalCode"

const user: User = {
  name: "John Doe",
  age: 30,
  address: {
    city: "New York",
    postalCode: 10001,
  },
};

// deep-keyof ライブラリを使用して、ユーザーの深いプロパティにアクセスできます
console.log(user.name); // "John Doe"
console.log(user.address.city); // "New York"
console.log(user["address.postalCode"]); // 10001




reduce 関数を使用する

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

type User = {
  name: string;
  age: number;
  address: {
    city: string;
    postalCode: number;
  };
};

type DeepKeys = DeepKeyOf<User>; // "name" | "age" | "address.city" | "address.postalCode"

const user: User = {
  name: "John Doe",
  age: 30,
  address: {
    city: "New York",
    postalCode: 10001,
  },
};

// reduce 関数を使用して、ユーザーの深いプロパティにアクセスできます
console.log(user.name); // "John Doe"
console.log(user.address.city); // "New York"
console.log(user["address.postalCode"]); // 10001

独自の型ガードを使用する

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

type IsObject<T> = T extends object ? true : false;

type DeepKeyOf<T> = IsObject<T> extends true
  ? {
      [K in keyof T]: K extends string | number
        ? K | `${K}.${DeepKeyOf<T[K]>}`
        : never;
    }
  : never;

type User = {
  name: string;
  age: number;
  address: {
    city: string;
    postalCode: number;
  };
};

type DeepKeys = DeepKeyOf<User>; // "name" | "age" | "address.city" | "address.postalCode"

const user: User = {
  name: "John Doe",
  age: 30,
  address: {
    city: "New York",
    postalCode: 10001,
  },
};

// 独自の型ガードを使用して、ユーザーの深いプロパティにアクセスできます
console.log(user.name); // "John Doe"
console.log(user.address.city); // "New York"
console.log(user["address.postalCode"]); // 10001

@ts-toolbelt/deep-keys ライブラリを使用する

import { DeepKeys } from "@ts-toolbelt/deep-keys";

type User = {
  name: string;
  age: number;
  address: {
    city: string;
    postalCode: number;
  };
};

type DeepKeys = DeepKeys<User>; // "name" | "age" | "address.city" | "address.postalCode"

const user: User = {
  name: "John Doe",
  age: 30,
  address: {
    city: "New York",
    postalCode: 10001,
  },
};

// @ts-toolbelt/deep-keys ライブラリを使用して、ユーザーの深いプロパティにアクセスできます
console.log(user.name); // "John Doe"
console.log(user.address.city); // "New York"
console.log(user["address.postalCode"]); // 10001

typescript


TypeScript コンパイラオプションの罠: module と target を正しく選択しないとどうなる?

TypeScript コンパイルオプション module と target は、どちらもコンパイルされた JavaScript コードの出力形式に影響を与える重要なオプションです。しかし、それぞれ異なる役割を果たします。target オプションは、TypeScript コンパイラが生成する JavaScript コードの ECMAScript レベルを指定します。これは、コンパイラが使用する言語機能と、出力されたコードが実行できる JavaScript エンジンを決定します。...


TypeScript ファイルで JavaScript モジュールをスマートにインポート! CommonJS と ES Module の徹底比較

TypeScript で JavaScript モジュールをインポートするには、主に以下の 2 つの方法があります。CommonJS 形式ES Module 形式それぞれの形式について、詳細と利点・欠点、そして実際にどのようにインポートするかを見ていきましょう。...


Angular、TypeScript、RxJSで発生する「TypeError: You provided an invalid object where a stream was expected. You can provide an Observable, Promise, Array, or Iterable」エラーを徹底解説

このエラーは、Angular、TypeScript、RxJS を使用した開発において、非同期処理に関わるコードで発生します。具体的には、Observable、Promise、Array、Iterable などのいずれかを期待していたにもかかわらず、無効なオブジェクトが渡された場合に発生します。...


Derive union type from tuple/array values in TypeScript

最も簡単な方法は、as 演算子を使うことです。例えば、以下のコードでは、tuple の要素の型から MyUnion というユニオン型を導出しています。as 演算子は、タプル型だけでなく、配列型にも使用できます。TypeScript 4.0以降では、infer キーワードを使って、より安全で柔軟な方法でユニオン型を導出することができます。...


「Cannot find namespace 'ctx'」エラーはもう怖くない! React、TypeScript、Ionic4 でコンテキストをマスターする

React、TypeScript、Ionic4 を使用してコンテキストを作成しようとすると、「名前空間 'ctx' を見つかりません」というエラーが発生することがあります。原因:このエラーは、通常、ctx という名前の変数が宣言されていないために発生します。コンテキスト API は、コンテキスト オブジェクトを現在のコンポーネント ツリー全体で共有するために使用されます。このオブジェクトには、コンポーネント間で共有する必要があるデータを含めることができます。...