TypeScriptの条件型(Conditional Types)の基本と応用を徹底解説

TypeScriptの条件型は、型システムにおける強力な機能であり、柔軟性と型安全性を兼ね備えています。この機能を活用することで、条件に応じて型を変えることができ、より動的なプログラミングが可能になります。本記事では、条件型の基本概念から、具体的な使用例、そして実践的な応用までを詳しく解説します。これにより、TypeScriptの条件型を使いこなし、より効率的なコードを書くための知識を得られることでしょう。

目次

条件型の基本概念

条件型は、TypeScriptにおける型の選択を行うための構文で、与えられた条件に基づいて異なる型を返すことができます。この機能は、型の動的な変更を可能にし、コードの柔軟性を高めます。

基本的な構文

条件型は次のように記述されます。

T extends U ? X : Y

ここで、TUを拡張する場合、型Xが返され、そうでない場合は型Yが返されます。このシンプルな構文により、複雑な型の制御が可能になります。

使用例

以下の例では、与えられた型が文字列であるかどうかを判定し、それに応じて異なる型を返します。

type IsString<T> = T extends string ? "Yes" : "No";

この場合、IsString<number>"No"を、IsString<string>"Yes"を返します。

条件型を理解することで、TypeScriptの型システムをさらに活用できるようになります。次のセクションでは、条件型の入れ子構造について詳しく見ていきましょう。

基本的な条件型の例

条件型を用いることで、型の選択や変換を簡単に行うことができます。ここでは、いくつかの基本的な使用例を通じて、条件型の具体的な動作を理解しましょう。

例1: 型の判定

まず、基本的な型の判定を行う条件型の例です。次のコードでは、IsNumberという型を定義しています。

type IsNumber<T> = T extends number ? "Number" : "Not a number";

この型を使うと、与えられた型が数値かどうかを判定できます。

type Test1 = IsNumber<number>; // "Number"
type Test2 = IsNumber<string>; // "Not a number"

例2: 複数の条件を持つ型

次に、複数の条件を持つ条件型の例です。以下のResponseType型では、引数の型によって異なるレスポンスを定義します。

type ResponseType<T> = T extends "success" ? { success: true; data: string } :
                       T extends "error" ? { success: false; message: string } :
                       never;

この型を使うと、以下のように異なるレスポンス型を得ることができます。

type SuccessResponse = ResponseType<"success">; // { success: true; data: string }
type ErrorResponse = ResponseType<"error">;     // { success: false; message: string }

例3: デフォルト値を持つ条件型

条件型にはデフォルト値を設定することも可能です。次の例では、引数が指定されない場合にデフォルト値を返す条件型を示します。

type DefaultValue<T = "default"> = T extends "default" ? "No value provided" : T;
type Value1 = DefaultValue;         // "No value provided"
type Value2 = DefaultValue<"data">; // "data"

これらの基本的な条件型の例を通じて、TypeScriptにおける条件型の柔軟性と強力さを実感できるでしょう。次のセクションでは、条件型の入れ子構造についてさらに深掘りしていきます。

条件型の入れ子構造

条件型は、他の条件型を内包することができるため、入れ子構造を持つことが可能です。これにより、より複雑な条件に基づく型の定義が実現できます。以下では、条件型の入れ子使用法について具体的に見ていきましょう。

入れ子条件型の基本例

入れ子条件型を用いることで、複数の条件を順に評価することができます。以下の例では、数値、文字列、またはその他の型を判定します。

type TypeChecker<T> = 
    T extends number ? "This is a number" : 
    T extends string ? "This is a string" : 
    "This is something else";

このTypeChecker型を使うと、以下のように異なる型に対する判定結果が得られます。

type Test1 = TypeChecker<number>; // "This is a number"
type Test2 = TypeChecker<string>; // "This is a string"
type Test3 = TypeChecker<boolean>; // "This is something else"

複数の条件を組み合わせる例

次に、入れ子の条件型を用いて、複数の条件を組み合わせてより詳細な型を定義します。以下の例では、型が数値の場合にさらにその数値が正か負かを判定します。

type NumberSign<T> = 
    T extends number ? 
    (T extends 0 ? "Zero" : T extends 1 | 2 | 3 ? "Positive" : "Negative") : 
    "Not a number";

このNumberSign型では、以下のように判定が行われます。

type Test1 = NumberSign<0>; // "Zero"
type Test2 = NumberSign<5>; // "Positive"
type Test3 = NumberSign<-3>; // "Negative"
type Test4 = NumberSign<string>; // "Not a number"

入れ子構造の実用例

入れ子の条件型は、実際のプロジェクトでも役立ちます。例えば、APIからのレスポンス型を条件によって動的に変更する際に利用できます。

type ApiResponse<T> = 
    T extends { success: true } ? { data: string } : 
    T extends { success: false } ? { error: string } : 
    never;

このように、条件型を入れ子にすることで、複雑な型の論理を簡潔に表現することができます。次のセクションでは、条件型を利用した型の絞り込みについて解説します。

条件型を利用した型の絞り込み

条件型は、特定の条件に基づいて型を絞り込むのに非常に便利です。このセクションでは、条件型を用いた型の絞り込みの方法を具体的な例とともに解説します。

基本的な絞り込みの例

型の絞り込みでは、ある型が特定の条件を満たすかどうかをチェックし、満たす場合には別の型を返すことができます。以下の例では、配列が空かどうかを判定する型を定義します。

type IsArrayEmpty<T> = T extends Array<infer U> ? (U extends never ? "Empty" : "Not empty") : "Not an array";

この型を使うと、与えられた引数が配列かつ空かどうかを確認できます。

type Test1 = IsArrayEmpty<[]>; // "Empty"
type Test2 = IsArrayEmpty<[1, 2, 3]>; // "Not empty"
type Test3 = IsArrayEmpty<number[]>; // "Not empty"
type Test4 = IsArrayEmpty<string>; // "Not an array"

条件型を用いたユニオン型の絞り込み

ユニオン型の中から特定の型を絞り込むことも可能です。以下の例では、ユニオン型から数値だけを抽出する型を定義します。

type ExtractNumbers<T> = T extends number ? T : never;

この型を使用すると、ユニオン型の中から数値だけを抽出できます。

type MixedType = string | number | boolean;
type OnlyNumbers = ExtractNumbers<MixedType>; // number

絞り込みを利用した条件型の応用例

条件型を用いた型の絞り込みは、実際のアプリケーションでの利用にも役立ちます。たとえば、APIからのレスポンスが成功か失敗かに応じて異なる処理を行う場合に、型を絞り込むことができます。

type ApiResponse<T> = 
    T extends { success: true; data: infer D } ? D : 
    T extends { success: false; error: infer E } ? E : 
    never;

このように、条件型を使った型の絞り込みを駆使することで、型安全なプログラミングが実現できます。次のセクションでは、条件型とマッピング型の組み合わせについて詳しく解説します。

条件型とマッピング型の組み合わせ

条件型とマッピング型を組み合わせることで、より柔軟で強力な型定義が可能になります。このセクションでは、両者を組み合わせた具体的な使用例を見ていきます。

マッピング型の基本

マッピング型は、既存の型に基づいて新しい型を生成する方法です。例えば、オブジェクトの各プロパティに対して変換を行うことができます。

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

type ReadOnly<T> = {
    readonly [K in keyof T]: T[K];
};

type ReadOnlyUser = ReadOnly<User>;

このReadOnly型を使うと、すべてのプロパティが読み取り専用のReadOnlyUser型を作成できます。

条件型との組み合わせ

条件型をマッピング型に組み合わせることで、プロパティの型に応じて異なる変換を行うことができます。次の例では、オブジェクトのプロパティが文字列かどうかを判定し、文字列の場合はそのまま、そうでない場合はneverを返す型を定義します。

type FilterStrings<T> = {
    [K in keyof T]: T[K] extends string ? T[K] : never;
};

type Sample = {
    id: number;
    name: string;
    age: number;
};

type StringProperties = FilterStrings<Sample>; // { name: string; age: never; id: never; }

この例では、StringProperties型がnameプロパティだけを持つことになります。

実用的な応用例

条件型とマッピング型の組み合わせは、実際の開発でも役立ちます。たとえば、APIレスポンスの型を加工する際に、特定のプロパティだけを抽出する場合に利用できます。

type ApiResponse<T> = {
    [K in keyof T]: T[K] extends { success: true } ? T[K] : never;
};

type Response = {
    data: { success: true; value: string };
    error: { success: false; message: string };
};

type SuccessResponse = ApiResponse<Response>; // { data: { success: true; value: string }; error: never; }

このように、条件型とマッピング型を組み合わせることで、型の操作がより強力になり、複雑な型の構造を簡潔に表現できるようになります。次のセクションでは、条件型の制約と注意点について解説します。

条件型の制約と注意点

条件型は強力な機能ですが、使用する際にはいくつかの制約や注意点があります。このセクションでは、条件型を使用する上での重要なポイントを解説します。

1. 型の評価順序

条件型は、左から右へと評価されるため、入れ子構造を使う際には、評価の順序を意識する必要があります。例えば、以下のような条件型があるとします。

type Example<T> = T extends string ? "String" : T extends number ? "Number" : "Other";

この場合、最初に文字列かどうかを評価し、その後に数値かどうかを評価します。順序が逆になると、期待通りの結果が得られないことがあります。

2. `never`型の取り扱い

条件型がneverを返す場合、型の選択肢から除外されるため、使用する際には注意が必要です。例えば、以下のように定義した条件型では、与えられた型が適合しない場合にneverを返します。

type IsString<T> = T extends string ? "Yes" : never;

この場合、IsString<number>neverとなり、型の選択肢から消えてしまいます。この挙動を理解しておくことが重要です。

3. 再帰的な条件型の制限

条件型は再帰的に使用することができますが、再帰の深さに制限があります。TypeScriptはデフォルトで最大深さが指定されており、これを超えるとエラーが発生します。再帰を使用する際には、深さに注意が必要です。

4. 型の明示性を保つ

条件型を多用すると、型の可読性が低下することがあります。特に複雑な条件型を使用する際には、型の意味を明示的にすることが重要です。コメントや別の型定義を使用して、理解しやすく保つことを心がけましょう。

// 例
type ComplexType<T> = T extends string 
    ? "It's a string" 
    : T extends number 
    ? "It's a number" 
    : "Unknown type"; // 明示的に型を示す

5. サポートされる型の種類

条件型はすべての型に対して適用できるわけではありません。例えば、シンボル型やマップ型に対しては、条件型がうまく機能しない場合があります。条件型の利用を考える際には、どのような型が対象となるかを考慮する必要があります。

これらの制約や注意点を理解することで、条件型を効果的に活用し、型安全で柔軟なコードを書くことができるようになります。次のセクションでは、実践的な応用例を紹介します。

実践的な応用例

条件型は、実際のプロジェクトにおいてさまざまな場面で活用できます。このセクションでは、条件型の具体的な応用例をいくつか紹介し、その使い方を理解します。

1. APIレスポンスの型定義

APIからのレスポンスは、成功時と失敗時で異なる構造を持つことが一般的です。条件型を用いることで、これらのレスポンスを型安全に処理できます。

type ApiResponse<T> = 
    T extends { success: true; data: infer D } ? D :
    T extends { success: false; error: infer E } ? E :
    never;

// 使用例
type Response1 = { success: true; data: { value: string } };
type Response2 = { success: false; error: { message: string } };

type Data1 = ApiResponse<Response1>; // { value: string }
type Error1 = ApiResponse<Response2>; // { message: string }

2. ユニオン型からの型抽出

ユニオン型から特定の型だけを抽出する際にも、条件型は非常に役立ちます。たとえば、複数の型がある場合に、特定の型だけをフィルタリングすることができます。

type ExtractStrings<T> = T extends string ? T : never;

type MixedTypes = string | number | boolean | null;
type OnlyStrings = ExtractStrings<MixedTypes>; // string

3. フォームデータの型検証

フォームデータの型を検証するために条件型を使用することで、入力に応じた型を動的に生成できます。

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

type UserForm = {
    name: string;
    age: number;
};

type ValidatedForm = FormData<UserForm>; // { name: string; age: number; }

4. 複雑なオブジェクト型の変換

条件型を利用して、複雑なオブジェクトの型を変換することもできます。たとえば、プロパティが特定の型である場合に別の型に変換することができます。

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

type Original = { id: number; name: string; age: number };
type Transformed = Transform<Original>; // { id: string; name: string; age: string }

5. 型の精緻化と制約の付加

条件型を用いることで、型に対してさらなる制約を加えることができます。たとえば、特定の型が指定された場合にのみ動作するような型を定義できます。

type OnlyStrings<T> = T extends string ? T : never;

function handleString<T>(input: OnlyStrings<T>) {
    console.log(input.toUpperCase());
}

handleString("hello"); // 正常
// handleString(123); // エラー

これらの応用例を通じて、条件型がどのように実践的な問題解決に役立つかを理解できるでしょう。次のセクションでは、条件型を利用した簡単な演習問題を提供し、さらに理解を深めます。

演習問題

このセクションでは、条件型に関する理解を深めるための演習問題を用意しました。各問題を解いて、条件型の活用方法を実践的に学びましょう。

問題1: 型の判定

次の条件型IsBooleanを完成させて、引数がブール型の場合に”Yes”、そうでない場合に”No”を返すようにしてください。

type IsBoolean<T> = T extends boolean ? "Yes" : "No";

// テスト
type Test1 = IsBoolean<boolean>; // ?
type Test2 = IsBoolean<number>;  // ?

問題2: ユニオン型の抽出

ユニオン型から文字列型だけを抽出するExtractStrings型を作成してください。

type ExtractStrings<T> = T extends string ? T : never;

// テスト
type Mixed = string | number | boolean;
type Result = ExtractStrings<Mixed>; // ?

問題3: APIレスポンスの型定義

APIからのレスポンスが成功時と失敗時で異なる型を持つ場合、条件型を使ってレスポンスの型を動的に決定するApiResponse型を作成してください。

type ApiResponse<T> = 
    T extends { success: true; data: infer D } ? D :
    T extends { success: false; error: infer E } ? E :
    never;

// テスト
type SuccessResponse = { success: true; data: { value: string } };
type ErrorResponse = { success: false; error: { message: string } };

type DataResult = ApiResponse<SuccessResponse>; // ?
type ErrorResult = ApiResponse<ErrorResponse>; // ?

問題4: オブジェクトの型変換

オブジェクトの各プロパティが数値型の場合に文字列型に変換するTransformNumbers型を作成してください。

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

// テスト
type Original = { id: number; name: string; age: number };
type Transformed = TransformNumbers<Original>; // ?

問題5: フォームデータの型検証

フォームデータの型を検証し、すべてのプロパティが文字列型に変換されるFormData<T>型を作成してください。

type FormData<T> = {
    [K in keyof T]: string;
};

// テスト
type UserForm = {
    name: string;
    age: number;
};
type ValidatedForm = FormData<UserForm>; // ?

これらの演習問題を通じて、条件型の使い方を実践し、理解を深めてください。次のセクションでは、これまでの内容をまとめます。

まとめ

本記事では、TypeScriptにおける条件型(Conditional Types)の基本から応用まで、さまざまな側面を詳しく解説しました。以下に主要なポイントを振り返ります。

  • 条件型の基本概念: 条件型を用いることで、型の選択を動的に行うことが可能で、型の安全性と柔軟性を向上させます。
  • 入れ子構造の利用: 条件型は入れ子で使用することができ、複雑な条件を順に評価することで、より詳細な型定義が可能になります。
  • 型の絞り込み: 条件型を利用して、特定の条件を満たす型だけを抽出することができ、ユニオン型からの型のフィルタリングなどに役立ちます。
  • マッピング型との組み合わせ: 条件型とマッピング型を組み合わせることで、オブジェクトのプロパティに基づいた型変換が容易になり、より複雑な型の操作が可能です。
  • 実践的な応用例: APIレスポンスの型定義やフォームデータの型検証、オブジェクトの型変換など、実際の開発での利用ケースを通じて条件型の重要性を理解しました。
  • 演習問題: 演習問題を通じて、条件型の理解を深め、実際に手を動かして学ぶ機会を提供しました。

これらの知識を活用することで、より型安全で効率的なTypeScriptのコードを書くことができるようになります。条件型をマスターして、強力な型システムを活用していきましょう。

コメント

コメントする

目次