読み込み中...

【TypeScript】複合型を完全マスター!10のコードで理解を深めよう

TypeScript複合型のイラストと10のサンプルコード TypeScript
この記事は約31分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

TypeScriptは、JavaScriptのスーパーセットとして広く利用されている静的型付け言語です。

この言語には多くの機能がありますが、今回は特に「複合型」という機能に焦点を当てて解説していきます。

複合型を使えば、変数や関数の型として複数の型を組み合わせることができます。

これにより、より柔軟で強力なプログラムを作成することが可能になります。

この記事の目標は、TypeScriptの複合型を「完全に理解する」ことです。

あなたが初心者であっても、この記事を読めば、複合型の使い方や応用法を理解し、実際のプログラミングで応用することができるようになるはずです。

それでは、TypeScriptの魅力的な機能である複合型を、一緒に学びましょう!

●TypeScriptの複合型とは

TypeScriptは、静的型言語であるため、変数や関数、クラスなどに型を付与することができます。

この特徴は、エラーの早期発見やコードの可読性向上に役立ちます。

特に、TypeScriptが持つ複合型は、複雑なデータ構造や関数の引数・返り値の型を効果的に表現するための強力なツールとして注目を集めています。

複合型とは、一言で言えば、いくつかの型を組み合わせて新しい型を作成することができる型のことを指します。

これにより、非常に柔軟かつ詳細な型定義を行うことが可能となります。

○複合型の基本

複合型の基本として、Union型とIntersection型が挙げられます。

それぞれの型の特性と基本的な使用方法を解説します。

□Union型(|を使用する)

この型は、「または」を意味します。

つまり、いくつかの型のうちの1つの型として値を持つことができます。

このコードでは、文字列または数字を受け取ることができる関数を定義しています。

この例では、stringまたはnumber型の値を受け取り、その値をそのまま返す関数を定義しています。

  function getValue(value: string | number): string | number {
      // 関数内部での処理
      return value;
  }

上記のコードでgetValue関数に文字列や数字を渡すと、それをそのまま返します。

例えば、getValue("Hello")とすると”Hello”を、getValue(123)とすると123を返します。

□Intersection型(&を使用する)

この型は、「かつ」という意味を持ちます。

つまり、複数の型の特性をすべて持つ新しい型を作成することができます。

このコードでは、2つのオブジェクト型を組み合わせて新しい型を作成しています。

この例では、Person型とJob型の特性をすべて持つ新しい型を定義しています。

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

  type Job = {
      jobTitle: string;
      salary: number;
  };

  type Employee = Person & Job;

  const employee: Employee = {
      name: "Taro",
      age: 30,
      jobTitle: "Engineer",
      salary: 500000
  };

上記のコードでは、Employee型として、Person型とJob型の両方のプロパティを持つオブジェクトを定義しています。

このため、employeeオブジェクトは、name, age, jobTitle, salaryの4つのプロパティを持っていることが分かります。

●複合型の使い方

TypeScriptでは、単一の型だけでなく、複数の型を組み合わせて使うことができる「複合型」が提供されています。

この複合型を活用することで、より柔軟かつ明確に変数や関数の型を表現することができます。

特にTypeScript初心者の方々には、この複合型を理解することが、TypeScriptの真価を十分に発揮する鍵となります。

○サンプルコード1:Union型の基本

Union型は、複数の型の中から1つの型を持つことができるものです。

| を使用して複数の型を組み合わせることで、Union型を定義できます。

// Union型の定義
let value: string | number;

// string型の値を代入
value = "hello";
console.log(value);  // hello

// number型の値を代入
value = 123;
console.log(value);  // 123

このコードでは、stringnumber の2つの型を持つことができる value という変数を定義しています。

この例では、最初に string 型の "hello" を代入し、次に number 型の 123 を代入しています。

このようにUnion型は、複数の型を持つ変数や関数の引数などに使用することができ、その変数や引数が取りうる値の型の範囲を柔軟に表現することができます。

また、このサンプルコードを実行すると、コンソールには先に "hello" が出力され、次に 123 が出力されることになります。

また、Union型は関数の引数にも適用することもできます。

Union型を引数として持つ関数の例を紹介します。

// 関数の引数にUnion型を適用
function printValue(value: string | number) {
    console.log(value);
}

printValue("TypeScript");
printValue(2023);

この関数printValueは、stringまたはnumberの型の引数valueを受け取り、その引数をコンソールに出力します。

そのため、この関数には文字列や数字を渡すことができ、それらがそのままコンソールに出力されることになります。

このサンプルコードを実行すると、まず"TypeScript"がコンソールに出力され、次に2023が出力されます。

○サンプルコード2:Intersection型の活用

TypeScriptには、多くの強力な型機能が備わっており、Intersection型はその中でも特にユニークな存在と言えます。

Intersection型は、その名前が示すように、複数の型を一つに結合して新しい型を作成するものです。

これにより、複数のオブジェクトの属性やメソッドを組み合わせて、新しいオブジェクトを作成することができます。

このコードでは、Intersection型を使って二つのオブジェクトの型を結合して新しい型を作成するコードを表しています。

この例では、Person型とJob型を結合してEmployee型を作成しています。

// Person型の定義
type Person = {
  name: string;
  age: number;
};

// Job型の定義
type Job = {
  title: string;
  salary: number;
};

// Intersection型を使用して、PersonとJobを結合したEmployee型を定義
type Employee = Person & Job;

// Employee型のオブジェクトを作成
const employee: Employee = {
  name: '山田太郎',
  age: 30,
  title: 'エンジニア',
  salary: 500000
};

console.log(employee);

上記のサンプルでは、まずPerson型とJob型を定義しています。

次に、&演算子を使用してこれらの型を結合し、新しいEmployee型を作成しています。

最後に、この新しい型を使用してemployeeオブジェクトを作成し、コンソールに出力しています。

このサンプルコードを実行すると、コンソールには次のようにemployeeオブジェクトの内容が出力されます。

山田太郎さんは、30歳のエンジニアとして月収500000円で働いています。

Intersection型は、異なる型に共通の属性やメソッドがある場合や、一つのオブジェクトが複数の役割を持つ必要がある場合に非常に役立ちます。

しかし、使用する際は型の結合に注意が必要です。

正しく型を組み合わせないと、想定外のエラーが発生することも考えられます。

続いて、Intersection型の応用例を見ていきましょう。

例えば、Employee型に研修期間を持たせたい場合は、新たな型TrainingPeriodを定義して、これをEmployee型と結合することができます。

// 研修期間を持つ型の定義
type TrainingPeriod = {
  trainingDays: number;
};

// Employee型とTrainingPeriod型を結合
type Trainee = Employee & TrainingPeriod;

// Trainee型のオブジェクトを作成
const trainee: Trainee = {
  ...employee,
  trainingDays: 60
};

console.log(trainee);

このコードでは、研修期間を持つ新しい型TrainingPeriodを定義し、これを先ほどのEmployee型と結合してTrainee型を作成しています。

この新しい型を使用して、研修期間を60日としたtraineeオブジェクトを作成し、その内容をコンソールに出力しています。

このサンプルコードを実行すると、山田太郎さんはエンジニアとして入社し、研修期間は60日間という情報がコンソールに表示されるでしょう。

Intersection型を活用すれば、既存の型を再利用しながら、必要に応じて新しい型を追加することが容易になります。

しかし、多くの型を結合しすぎるとコードが複雑になる可能性もあるため、適切な組み合わせとバランスが求められます。

○サンプルコード3:Type Aliasを使った複合型定義

TypeScriptで型定義を行う際、単純な型だけでなく、カスタマイズした型も作成できます。

特に「Type Alias」は、複合型を効果的に活用するキーとなる機能です。

Type Aliasを利用することで、独自の型を作成し、コードの可読性を高めることができます。

まず、基本的なType Aliasの使用方法を見てみましょう。

// Type Aliasの定義
type StringOrNumber = string | number;

// Type Aliasを使用する
let value: StringOrNumber;
value = 'Hello, TypeScript!';
value = 123;

このコードでは「StringOrNumber」というType Aliasを定義しています。

この例では、文字列型(string)と数値型(number)のUnion型を使って「StringOrNumber」という新しい型を作成しています。

その後、このType Aliasを変数valueの型として指定することで、valueには文字列または数値を代入することが可能となります。

さて、上記のコードを実行すると、変数valueには文字列「Hello, TypeScript!」と数値123が順番に代入され、エラーが発生しないことが確認できます。

これにより、Type Aliasを活用して、コードの柔軟性を保ちながらも、型の安全性を維持することができます。

Type Aliasは複雑な型の定義にも使用することができます。

例えば、オブジェクトの形状を定義する場合などにも利用可能です。

// オブジェクトの形状をType Aliasで定義
type UserProfile = {
  name: string;
  age: number;
  address?: string; // 任意のプロパティ
};

// Type Aliasを使用する
let user: UserProfile = {
  name: 'Taro',
  age: 25,
  address: 'Tokyo'
};

この例では、「UserProfile」というType Aliasを作成しており、この型にはnameage、および任意のaddressという3つのプロパティが定義されています。

このType Aliasを変数userの型として指定することで、指定された形状のオブジェクトを作成することができます。

このコードを実行すると、変数userには指定した形状のオブジェクトが代入され、エラーが発生しません。

また、addressは任意のプロパティとして定義されているため、このプロパティを省略しても問題ありません。

○サンプルコード4:Literal型での具体的な値の指定

TypeScriptの中でも、特定の値のみを取り扱いたい場合に使用するLiteral型。文字列や数値、boolean値など、具体的な値を指定することが可能です。

ここでは、Literal型の使い方に焦点を当て、その活用方法をサンプルコードを交えて詳しく解説していきます。

このコードではLiteral型を使用して、特定の文字列のみを許容する変数を定義しています。

この例では、”apple”, “orange”, “banana”の3つの文字列のみを受け入れる変数を定義しています。

type Fruit = "apple" | "orange" | "banana";

let myFruit: Fruit;

myFruit = "apple";    // この代入は問題ありません。
// myFruit = "grape"; // コンパイルエラー。許容されていない文字列を代入しようとするとエラーになります。

上記のサンプルコードでは、Fruitという型をLiteral型で定義しています。

このFruit型は”apple”、”orange”、”banana”の3つの文字列のみを許容しています。

したがって、それ以外の文字列を代入しようとすると、TypeScriptのコンパイラがエラーを発生させます。

例えば、上のコードのコメントアウトされている部分を見てみると、”grape”という許容されていない文字列をmyFruitに代入しようとしているため、エラーとなることがわかります。

しかし、Literal型の強力な点は、文字列だけでなく、数値やboolean値も許容することができる点です。

下記のサンプルコードでは、数値のLiteral型の例を表しています。

type Age = 20 | 25 | 30;

let myAge: Age;

myAge = 25;  // この代入は問題ありません。
// myAge = 22; // コンパイルエラー。許容されていない数値を代入するとエラーになります。

このように、特定の数値や文字列、boolean値を制限して、その範囲内でのみ変数の操作を許容するというのがLiteral型の大きな特徴です。

Literal型を用いることで、コードの品質を高めることができます。

不正な値の代入や予期しない動作を事前に避けることができるのです。

一方、Literal型を使用する際の注意点としては、利用する値が増えると型定義が複雑になり、管理が煩雑になる可能性があるため、適切な場面や値の範囲で使用することが推奨されます。

最後に、Literal型は他の型と組み合わせても利用することができます。

例えば、Union型やType Aliasと組み合わせて、さらに柔軟な型定義を行うことも可能です。

●複合型の応用例

TypeScriptには多様な複合型が提供されていますが、それらを実際のコードに応用することで、より高度な型安全性やコードの再利用性を向上させることができます。

ここでは、複合型を用いた具体的な応用例をいくつかサンプルコードとともに紹介します。

○サンプルコード5:関数の引数でのUnion型の利用

まず最初に、関数の引数としてUnion型を活用する方法を取り上げます。

Union型を関数の引数に使うことで、複数の異なる型の値を一つの引数として受け取ることができるようになります。

type Animal = "dog" | "cat" | "bird";

function getAnimalSound(animal: Animal): string {
    switch (animal) {
        case "dog":
            return "ワンワン";
        case "cat":
            return "ニャー";
        case "bird":
            return "ピーピー";
    }
}

// コメント: 上記の関数を呼び出す例
console.log(getAnimalSound("dog"));  // ワンワン
console.log(getAnimalSound("cat"));  // ニャー

このコードでは、AnimalというUnion型を使って、”dog”、”cat”、”bird”の3つの文字列型を定義しています。

その後、このAnimal型を引数として受け取る関数getAnimalSoundを実装しました。

この例では、異なる動物の名前に応じて、それぞれの動物の鳴き声を返す機能を持つ関数を作成しています。

このコードを実行すると、それぞれの動物の鳴き声がコンソールに出力されます。

具体的には、”ワンワン”や”ニャー”といった文字列が表示されるでしょう。

上記のコードでは、Union型Animalを使用して、3つの異なる文字列型の値を一つの型として扱っています。

これにより、getAnimalSound関数は、Animal型のどれか一つの値しか受け取ることができなくなります。

そのため、この関数に無効な動物の名前を渡すと、TypeScriptの型チェック時にエラーが発生し、バグの早期発見が可能となります。

また、この関数内では、引数として受け取った動物の名前に応じて、適切な動物の鳴き声を返す処理をswitch文を使って実装しています。

このようにUnion型は、限定された値のいずれか一つを受け取ることを保証するため、不正な値が関数内で処理されるリスクを大幅に減少させることができます。

応用として、このようなUnion型を活用することで、関数の引数や返り値の型を狭めることができ、コードの安全性を高めるとともに、意図しない値の使用を事前に排除することができるのです。

○サンプルコード6:共用体を活用した関数のオーバーロード

共用体とは、TypeScriptにおいて複数の型を一つの型として扱うことができる特殊な型です。

たとえば、ある関数が整数または文字列を受け取ることができる場合、この関数の引数の型として共用体を使用することができます。

ここでは、このような共用体を活用した関数のオーバーロードについての説明と、その具体的なコード例を提供します。

共用体を活用した関数のオーバーロードの基本的なサンプルコードを紹介します。

// 共用体の定義
type NumberOrString = number | string;

// オーバーロードされた関数の定義
function displayValue(value: NumberOrString) {
    if (typeof value === "number") {
        console.log(`数値: ${value}`);
    } else {
        console.log(`文字列: ${value}`);
    }
}

// コードの実行
displayValue(10);   // 数値: 10
displayValue("こんにちは");  // 文字列: こんにちは

このコードでは、NumberOrStringという共用体を定義しています。

この共用体はnumber型またはstring型のどちらかの値を持つことができます。

その後、displayValueという関数を定義しています。

この関数はNumberOrString型の引数を受け取り、受け取った引数の型に応じて異なるメッセージをコンソールに表示します。

この例では、10という数値を引数として関数を呼び出すと「数値: 10」と表示され、”こんにちは”という文字列を引数として関数を呼び出すと「文字列: こんにちは」と表示されます。

この方法を使用すると、異なる型の引数を受け取ることができる関数を簡単に作成することができます。

また、関数内でtypeof演算子を使用することにより、引数の型を簡単に確認することができます。

このコードのポイントは、TypeScriptの型システムを使用して、複数の型を持つことができる共用体を定義し、それを使用して関数のオーバーロードを実現することです。

この方法を使用することにより、関数が受け取ることができる引数の型を柔軟に管理することができます。

結果として、関数は10という数値と”こんにちは”という文字列の2つの異なる型の引数を受け取ることができ、それぞれの引数の型に応じて異なるメッセージをコンソールに表示することができます。

これにより、関数の再利用性と柔軟性が向上します。

○サンプルコード7:Discriminated Unionを活用したタイプガード

TypeScriptでの複合型を学んできたあなたに、さらなる応用テクニックを紹介します。

その名も「Discriminated Union」。

日本語に訳すと「区別された共用体」となります。

これは、リテラル型やUnion型と合わせて使用し、TypeScriptの強力なタイプガード機能を引き出す方法です。

このコードでは、Discriminated Unionを使って異なるオブジェクト型に共通の識別フィールドを持たせ、それを利用してタイプガードを行う例を表しています。

この例では、動物を表す2つの型「犬」および「鳥」に共通の識別フィールドtypeを持たせて区別しています。

interface 犬 {
    type: "犬";
    吠える(): void;
}

interface 鳥 {
    type: "鳥";
    鳴く(): void;
}

type 動物 = 犬 | 鳥;

function 動物の行動(動物: 動物) {
    if (動物.type === "犬") {
        動物.吠える();
    } else if (動物.type === "鳥") {
        動物.鳴く();
    }
}

このように、共通の識別フィールドを用いることで、どのような動物が来ても適切に処理を分岐することができます。

具体的には、犬ならば吠えるメソッドを、鳥ならば鳴くメソッドを呼び出すことが確定的にできます。

このコードを利用して、次のような動物のインスタンスを作成して動物の行動関数を呼び出すと、犬ならば吠え、鳥ならば鳴くという処理が行われます。

const ワンちゃん: 犬 = {
    type: "犬",
    吠える: () => {
        console.log("ワンワン!");
    }
}

const ピヨちゃん: 鳥 = {
    type: "鳥",
    鳴く: () => {
        console.log("ピヨピヨ!");
    }
}

動物の行動(ワンちゃん);  // ワンワン! とコンソールに表示されます
動物の行動(ピヨちゃん);  // ピヨピヨ! とコンソールに表示されます

Discriminated Unionのメリットは、型の安全性を維持しつつ、綺麗で直感的なコードを書くことができる点にあります。

特に、大規模なプロジェクトや複雑な型構造が存在する場合、これらの型を安全かつ効率的に扱うための強力なツールとなります。

応用例として、識別フィールドをさらに活用し、それぞれの動物に固有の特性や機能を追加することも可能です。

たとえば、鳥には「空を飛ぶ」というメソッドを追加し、その機能をDiscriminated Unionを活用して呼び出すことが考えられます。

○サンプルコード8:Mapped Typeを使った型の変換

TypeScriptでは、既存の型をもとに新しい型を動的に生成するための仕組みが提供されています。それが「Mapped Type(マップドタイプ)」です。

ここでは、Mapped Typeを利用して型の変換を行う方法を詳しく紹介していきます。

Mapped Typeは、既存の型の各プロパティを新しい型に変換するものです。

下記のサンプルコードでは、Person型のすべてのプロパティを読み取り専用に変換する新しい型ReadonlyPersonを生成しています。

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

type ReadonlyPerson = {
    readonly [K in keyof Person]: Person[K];
};

このコードでは、ReadonlyPerson型はPerson型の全てのプロパティを読み取り専用に変換した型として定義されています。

この例では、[K in keyof Person]という文法を使用して、Personのすべてのキー(nameage)に対して繰り返し処理を行い、それぞれのプロパティを読み取り専用にしています。

さらに、Mapped Typeを使うと、特定の型のすべてのプロパティの型を別の型に変換することも可能です。

例えば、下記のコードでは、Person型のすべてのプロパティの型を文字列に変換したStringifiedPerson型を定義しています。

type StringifiedPerson = {
    [K in keyof Person]: string;
};

このStringifiedPerson型を使用すると、nameageもどちらのプロパティも文字列として扱われるようになります。

これにより、ageプロパティの数値を文字列に変換して取り扱いたい場合などに便利です。

上記のサンプルコードを使用して、ReadonlyPerson型とStringifiedPerson型の動作を確認すると、次のような結果となります。

const person: Person = {
    name: "Taro",
    age: 25
};

const readonlyPerson: ReadonlyPerson = person;
readonlyPerson.name = "Jiro"; // エラー:nameは読み取り専用です

const stringifiedPerson: StringifiedPerson = {
    name: "Hanako",
    age: "30"  // ageプロパティが文字列として正常に受け入れられる
};

readonlyPersonnameプロパティに値を代入しようとすると、読み取り専用プロパティのためエラーが発生します。

一方、stringifiedPersonの場合は、ageプロパティが文字列として正常に受け入れられることが確認できます。

○サンプルコード9:Conditional Typesで条件に応じた型の生成

TypeScriptでは、型に条件を設定し、その条件に応じて型を動的に生成することができる機能として「Conditional Types(条件型)」が提供されています。

この強力な機能を利用することで、より柔軟な型定義や複雑な型の操作を行うことが可能となります。

このコードでは、条件型を使って特定の条件に基づいて型を生成する方法を表しています。

この例では、引数が配列であるかどうかを判定し、配列であればその要素の型を、そうでなければnever型を返す型を定義しています。

// TがArray型であればその要素の型を、そうでなければneverを返す
type ElementType<T> = T extends Array<infer U> ? U : never;

// 使用例
type NumberArray = number[];
type StringArray = string[];

// 数値の配列の場合、要素の型はnumberになる
type Result1 = ElementType<NumberArray>;  // type Result1 = number

// 文字列の配列の場合、要素の型はstringになる
type Result2 = ElementType<StringArray>;  // type Result2 = string

上の例では、ElementTypeという型を定義しています。この型は、Tが配列型(Array<U>)の場合、その配列の要素の型Uを返します。

Tが配列型でない場合には、never型を返します。

inferキーワードは、条件型の中で新しい型変数を導入するためのものです。

この例では、Tが配列であればその要素の型をUとして捉え、その型を返すようにしています。

このコードを実行すると、Result1number型、Result2string型として推論されます。

注意点として、条件型は複数の条件を組み合わせて使用することもできますが、複雑な型定義になりがちなので、利用する際は適切なコメントやドキュメントを残すことをおすすめします。

応用例として、次のように複数の型を組み合わせた条件型も考えられます。

// TがPromiseの場合、その解決値の型を返す。それ以外の場合はTをそのまま返す
type Unwrapped<T> = T extends Promise<infer U> ? U : T;

// 使用例
const promise: Promise<string> = Promise.resolve("hello");
type ResolvedType = Unwrapped<typeof promise>;  // type ResolvedType = string

この例では、Unwrappedという型を使って、TPromiseであればその解決値の型を返し、それ以外の場合はTをそのまま返すようにしています。

このように、条件型は非常に柔軟であり、多岐にわたる型の操作が可能です。

○サンプルコード10:Template Literal Typesの利用例

TypeScript 4.1で導入された「Template Literal Types(テンプレートリテラル型)」は、文字列リテラルを組み合わせて新しい文字列リテラル型を作成するための機能です。

これは、リテラル型をさらにパワフルにし、型レベルの文字列操作を可能にします。

このコードでは、Template Literal Typesを使って、文字列型の組み合わせを行うコードを表しています。

この例では、ユーザーの役職と名前を組み合わせて、完全な役職名を生成しています。

type Position = 'エンジニア' | 'デザイナー';
type Name = '田中' | '鈴木';

type FullName<Pos extends Position, N extends Name> = `${Pos}の${N}`;

const tanaka: FullName<'エンジニア', '田中'> = 'エンジニアの田中';
const suzuki: FullName<'デザイナー', '鈴木'> = 'デザイナーの鈴木';

上記のサンプルコードでは、PositionNameという二つのリテラル型を定義しています。

そして、Template Literal Typesを活用して、これらの文字列を組み合わせて新しい型FullNameを生成しています。

この時、テンプレートリテラルの中の${}内に型変数を指定することで、指定された型に応じた文字列の組み合わせが可能になります。

そして、実際の変数tanakasuzukiに対して、それぞれ異なる組み合わせの型を指定し、期待される文字列を代入しています。

このようにして、Template Literal Typesは、型安全性を保ったまま、動的な文字列の組み合わせを行うことができます。

次に、実際にTemplate Literal Typesを使った実行後のコードの動きを見ていきましょう。

TypeScriptコンパイラが上記のコードを検証すると、tanakasuzukiの変数に正しい文字列が代入されているかをチェックします。

もし、tanakaに「デザイナーの田中」という文字列を代入するようなコードを書いてしまった場合、コンパイルエラーが発生します。

これにより、事前に型の不整合や誤った文字列の組み合わせを検出できる利点があります。

また、Template Literal Typesの応用例として、APIのURLを組み立てる際にも役立ちます。

例えば、次のように、特定のAPIのエンドポイントとIDを組み合わせてURLを生成することが考えられます。

type Endpoint = 'users' | 'articles';
type ID = string;

type URL<EP extends Endpoint, I extends ID> = `https://api.example.com/${EP}/${I}`;

const userUrl: URL<'users', '1234'> = 'https://api.example.com/users/1234';

このコードでは、EndpointIDという二つの型を組み合わせて、URLという新しい型を生成しています。

そして、実際の変数userUrlに、期待されるURLを代入しています。

このように、Template Literal Typesは非常に柔軟な型操作を提供し、さまざまな場面での利用が期待されています。

●注意点と対処法

TypeScriptの複合型を活用することで、非常に柔軟なコードの記述が可能となりますが、その強力さゆえにトラブルの原因ともなりえます。

ここでは、複合型を使用する際の一般的な注意点と、それに対する対処法を紹介します。

○型の予期せぬ干渉

複数の複合型を一緒に使用すると、思わぬ型干渉が起こることがあります。

特にUnion型とIntersection型を併用する際は、注意が必要です。

このコードではUnion型とIntersection型を併用しています。

この例では、Union型で定義された型Aと、Intersection型で定義された型Bを組み合わせています。

type A = string | number;
type B = { name: string } & { age: number };

const sampleFunction = (arg: A & B) => {
  // 処理内容
}

こちらのコードを実行すると、argはstring | numberという型と{ name: string } & { age: number }という型の両方を満たす必要があるため、非常に制約の強い型となってしまいます。

このような場合、適切に型を組み合わせるか、関数の定義を見直す必要があります。

○Discriminated Unionの網羅的なチェックの欠如

Discriminated Unionを利用する際には、全てのケースを網羅しているか常に確認する必要があります。

一部を漏れてしまうと、ランタイムエラーの原因となる可能性があります。

このコードでは、Discriminated Unionを利用して動物の種類ごとの特性を表現しています。

この例では、動物の種類を表すtypeを定義し、それを基に各動物の特性を実装しています。

type Animal = { kind: 'dog', bark: () => void } | { kind: 'cat', meow: () => void };

const playWithAnimal = (animal: Animal) => {
  if (animal.kind === 'dog') {
    animal.bark();
  } else if (animal.kind === 'cat') {
    animal.meow();
  }
}

この関数は、与えられた動物の種類に応じて適切なメソッドを呼び出します。

しかし、Animal型に新たな種類を追加した際に、playWithAnimal関数の条件分岐を更新し忘れると、予期せぬエラーが発生する可能性が高まります。

●カスタマイズ方法

TypeScriptの複合型は強力ですが、プロジェクトのニーズに合わせてカスタマイズする方法も多く存在します。

ここでは、TypeScriptの複合型をさらに進化させるためのカスタマイズ方法を解説します。

○複合型のカスタマイズ

このコードでは、複合型をカスタマイズして新しい型を生成する方法を表しています。

この例では、Union型とType Aliasを組み合わせてカスタマイズしています。

// Type Aliasを使用してカスタマイズした複合型を定義
type CustomType = string | { name: string; age: number };

// カスタマイズした複合型を使用した関数
function displayData(data: CustomType) {
    if (typeof data === "string") {
        console.log(`名前: ${data}`);
    } else {
        console.log(`名前: ${data.name}, 年齢: ${data.age}`);
    }
}

displayData("山田太郎");
displayData({ name: "佐藤次郎", age: 25 });

上のコードを実行すると、次の出力が得られます。

名前: 山田太郎
名前: 佐藤次郎, 年齢: 25

このように、複合型をカスタマイズすることで、独自の型定義を作成し、コードの柔軟性を向上させることができます。

○既存の型に新しいプロパティを追加するカスタマイズ

このコードでは、既存の型に新しいプロパティを追加するカスタマイズの方法を表しています。

この例では、Intersection型を使用して2つの型を組み合わせています。

// 既存の型
type Person = {
    name: string;
    age: number;
};

// 新しいプロパティを追加した型
type ExtendedPerson = Person & { job: string };

const person: ExtendedPerson = {
    name: "鈴木一郎",
    age: 30,
    job: "エンジニア"
};

console.log(`${person.name}は${person.age}歳の${person.job}です。`);

上のコードを実行すると、次の出力が得られます。

鈴木一郎は30歳のエンジニアです。

このように、既存の型に新しいプロパティを追加することで、より詳細な型定義を作成することができます。

まとめ

今回の記事を通して、TypeScriptの複合型についての概要から、具体的な使い方、さらには応用テクニックまでを10のサンプルコードと共に紹介しました。

複合型はTypeScriptを使っての開発において非常に強力なツールであり、それを理解し適切に使うことで、より安全で綺麗なコードを書くことができます。

TypeScriptの力を最大限に引き出すためには、基本的な文法だけでなく、複合型のような高度なテクニックもしっかりと習得することが必要です。

本記事が、あなたのTypeScriptスキルアップの一助となれば幸いです。

日々の開発作業に役立てて、より品質の高いコードを書く手助けとしてください。