日本語

TypeScriptの強力なMapped TypesとConditional Typesの包括的ガイド。実践例と高度なユースケースを通して、堅牢で型安全なアプリケーションの構築法を解説します。

TypeScriptのMapped TypesとConditional Typesをマスターする

JavaScriptのスーパーセットであるTypeScriptは、堅牢で保守性の高いアプリケーションを作成するための強力な機能を提供します。これらの機能の中でも、Mapped TypesConditional Typesは、高度な型操作に不可欠なツールとして際立っています。このガイドでは、これらの概念の包括的な概要を提供し、その構文、実践的な応用、そして高度な使用例を探求します。あなたがベテランのTypeScript開発者であれ、旅を始めたばかりであれ、この記事はこれらの機能を効果的に活用するための知識をあなたに与えるでしょう。

Mapped Typesとは?

Mapped Typesは、既存の型を変換して新しい型を作成することを可能にします。既存の型のプロパティを反復処理し、各プロパティに変換を適用します。これは、すべてのプロパティをオプショナルにしたり、読み取り専用にしたりするなど、既存の型のバリエーションを作成するのに特に役立ちます。

基本的な構文

Mapped Typeの構文は以下の通りです:

type NewType<T> = {
  [K in keyof T]: Transformation;
};

実践的な例

プロパティを読み取り専用にする

ユーザープロフィールを表すインターフェースがあるとします:

interface UserProfile {
  name: string;
  age: number;
  email: string;
}

すべてのプロパティが読み取り専用の新しい型を作成できます:

type ReadOnlyUserProfile = {
  readonly [K in keyof UserProfile]: UserProfile[K];
};

これで、ReadOnlyUserProfileUserProfileと同じプロパティを持ちますが、それらはすべて読み取り専用になります。

プロパティをオプショナルにする

同様に、すべてのプロパティをオプショナルにすることができます:

type OptionalUserProfile = {
  [K in keyof UserProfile]?: UserProfile[K];
};

OptionalUserProfileUserProfileのすべてのプロパティを持ちますが、各プロパティはオプショナルになります。

プロパティの型を変更する

各プロパティの型を変更することもできます。例えば、すべてのプロパティを文字列に変換することができます:

type StringifiedUserProfile = {
  [K in keyof UserProfile]: string;
};

この場合、StringifiedUserProfileのすべてのプロパティはstring型になります。

Conditional Typesとは?

Conditional Typesは、条件に依存する型を定義することを可能にします。これは、型が特定の制約を満たすかどうかに基づいて型の関係を表現する方法を提供します。これはJavaScriptの三項演算子に似ていますが、型のためのものです。

基本的な構文

Conditional Typeの構文は以下の通りです:

T extends U ? X : Y

実践的な例

型が文字列かどうかを判断する

入力型が文字列の場合はstringを、それ以外の場合はnumberを返す型を作成してみましょう:

type StringOrNumber<T> = T extends string ? string : number;

type Result1 = StringOrNumber<string>;  // string
type Result2 = StringOrNumber<number>;  // number
type Result3 = StringOrNumber<boolean>; // number

ユニオンから型を抽出する

Conditional Typesを使用して、ユニオン型から特定の型を抽出できます。例えば、null許容でない型を抽出するには:

type NonNullable<T> = T extends null | undefined ? never : T;

type Result4 = NonNullable<string | null | undefined>; // string

ここで、Tnullまたはundefinedの場合、型はneverになり、TypeScriptのユニオン型の簡略化によって除外されます。

型を推論する

Conditional Typesは、inferキーワードを使用して型を推論するためにも使用できます。これにより、より複雑な型構造から型を抽出できます。

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

function myFunction(x: number): string {
  return x.toString();
}

type Result5 = ReturnType<typeof myFunction>; // string

この例では、ReturnTypeは関数の戻り値の型を抽出します。Tが任意の引数を取り、型Rを返す関数であるかどうかをチェックします。もしそうならRを返し、そうでなければanyを返します。

Mapped TypesとConditional Typesの組み合わせ

Mapped TypesとConditional Typesの真の力は、それらを組み合わせることで発揮されます。これにより、非常に柔軟で表現力豊かな型の変換を作成できます。

例:Deep Readonly

一般的な使用例として、ネストされたプロパティを含むオブジェクトのすべてのプロパティを読み取り専用にする型を作成することが挙げられます。これは再帰的なConditional Typeを使用して実現できます。

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
  };
}

type ReadonlyCompany = DeepReadonly<Company>;

ここで、DeepReadonlyはすべてのプロパティとそのネストされたプロパティに再帰的にreadonly修飾子を適用します。プロパティがオブジェクトの場合、そのオブジェクトに対して再帰的にDeepReadonlyを呼び出します。それ以外の場合は、単にプロパティにreadonly修飾子を適用します。

例:型によるプロパティのフィルタリング

特定の型のプロパティのみを含む型を作成したいとします。Mapped TypesとConditional Typesを組み合わせてこれを実現できます。

type FilterByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Person {
  name: string;
  age: number;
  isEmployed: boolean;
}

type StringProperties = FilterByType<Person, string>; // { name: string; }

type NonStringProperties = Omit<Person, keyof StringProperties>;

この例では、FilterByTypeTのプロパティを反復処理し、各プロパティの型がUを拡張するかどうかをチェックします。もしそうなら、結果の型にプロパティを含めます。そうでなければ、キーをneverにマッピングすることで除外します。「as」を使用してキーを再マッピングしていることに注意してください。次に、`Omit`と`keyof StringProperties`を使用して、元のインターフェースから文字列プロパティを削除します。

高度な使用例とパターン

基本的な例を超えて、Mapped TypesとConditional Typesは、より高度なシナリオで使用して、高度にカスタマイズ可能で型安全なアプリケーションを作成できます。

分配的なConditional Types

チェックされる型がユニオン型である場合、Conditional Typesは分配的になります。これは、条件がユニオンの各メンバーに個別に適用され、その結果が新しいユニオン型に結合されることを意味します。

type ToArray<T> = T extends any ? T[] : never;

type Result6 = ToArray<string | number>; // string[] | number[]

この例では、ToArrayはユニオンstring | numberの各メンバーに個別に適用され、結果としてstring[] | number[]になります。もし条件が分配的でなかった場合、結果は(string | number)[]になっていたでしょう。

ユーティリティ型の使用

TypeScriptは、Mapped TypesとConditional Typesを活用するいくつかの組み込みユーティリティ型を提供しています。これらのユーティリティ型は、より複雑な型変換の構成要素として使用できます。

これらのユーティリティ型は、複雑な型操作を簡素化できる強力なツールです。例えば、PickPartialを組み合わせて、特定のプロパティのみをオプショナルにする型を作成できます:

type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
}

type OptionalDescriptionProduct = Optional<Product, "description">;

この例では、OptionalDescriptionProductProductのすべてのプロパティを持ちますが、descriptionプロパティはオプショナルです。

テンプレートリテラル型の使用

テンプレートリテラル型は、文字列リテラルに基づいて型を作成することを可能にします。これらはMapped TypesやConditional Typesと組み合わせて、動的で表現力豊かな型変換を作成するために使用できます。例えば、すべてのプロパティ名に特定の文字列を接頭辞として付加する型を作成できます:

type Prefix<T, P extends string> = {
  [K in keyof T as `${P}${string & K}`]: T[K];
};

interface Settings {
  apiUrl: string;
  timeout: number;
}

type PrefixedSettings = Prefix<Settings, "data_">;

この例では、PrefixedSettingsdata_apiUrldata_timeoutというプロパティを持ちます。

ベストプラクティスと考慮事項

結論

Mapped TypesConditional Typesは、TypeScriptの強力な機能であり、非常に柔軟で表現力豊かな型の変換を作成できます。これらの概念をマスターすることで、TypeScriptアプリケーションの型安全性、保守性、そして全体的な品質を向上させることができます。プロパティをオプショナルや読み取り専用にするような単純な変換から、複雑な再帰的変換や条件付きロジックまで、これらの機能は堅牢でスケーラブルなアプリケーションを構築するために必要なツールを提供します。これらの機能のポテンシャルを最大限に引き出し、より熟練したTypeScript開発者になるために、探求と実験を続けてください。

TypeScriptの旅を続けるにあたり、公式のTypeScriptドキュメント、オンラインコミュニティ、オープンソースプロジェクトなど、利用可能な豊富なリソースを活用することを忘れないでください。Mapped TypesとConditional Typesの力を受け入れれば、最も困難な型関連の問題にも対処できるようになるでしょう。