[TypeScript] TypeScriptマスターガイド:基礎から応用まで完全網羅

1. TypeScriptとは?

TypeScript(TS)は、JavaScriptに型システムを追加することで、開発時にエラーを早期に発見できるようにするための言語です。TypeScriptはJavaScriptのスーパーセットであり、JavaScriptのすべての機能を備えていますが、さらに型チェックやインターフェース、クラスの高度な機能を提供します。

  • TypeScriptの特徴:

    • 静的型付け:変数や関数の引数に型を指定できるため、コードをコンパイル時に検証できます。
    • 型推論:明示的に型を指定しなくても、TypeScriptが型を推論してくれる場合があります。
    • 最新のJavaScript機能:TypeScriptは最新のECMAScript(ES6以上)の機能をサポートしており、コンパイル後にES3やES5に変換可能です。
  • なぜTypeScriptを使用するのか?: TypeScriptは、特に大規模なアプリケーションの開発時に役立ちます。型安全性を提供することによって、バグを早期に発見でき、コードの可読性や保守性も向上します。JavaScriptは動的型付けの言語であり、コードの実行時にエラーが発生することがありますが、TypeScriptはコンパイル時にエラーをチェックするため、事前に問題を解決できます。

1.1 JavaScriptとの違い

TypeScriptとJavaScriptの最大の違いは、型システムの有無です。JavaScriptでは型を指定せずにコードを書くことができますが、TypeScriptでは変数や関数に型を指定できます。

例えば、以下のようにTypeScriptでの型指定ができます:

let message: string = "Hello, TypeScript!";

上記のコードでは、message という変数に string 型が指定されており、この変数には文字列以外の値(例えば数値など)は代入できません。

■ TypeScriptとJavaScriptの主な違い:

  • 型安全: TypeScriptでは型を指定することで、誤った型のデータが代入されるのを防ぎます。
  • 型推論: TypeScriptは、明示的に型を指定しなくても、コードを分析して型を自動的に推論します。
  • クラスベースのオブジェクト指向: TypeScriptは、ES6のクラスをベースにしたオブジェクト指向プログラミングの機能を提供します。

1.2 型の追加がどのように役立つか

TypeScriptの型付けによって、以下のようなメリットがあります:

  1. エラーの早期発見: 型を追加することで、コンパイル時にエラーを発見できます。例えば、以下のコードはTypeScriptで型エラーが発生します。

    let num: number = 5;
    num = "Hello"; // 型エラー: 'string' は 'number' 型に割り当てできません。
    
  2. コードの補完とナビゲーション: 型定義により、エディタでのコード補完が強化されます。これにより、より効率的にコードを書くことができます。

  3. ドキュメンテーション: 型情報がコード内に明記されているため、コードを読んだときにその動作が直感的にわかりやすくなります。型が示す情報は、関数や変数の使い方を理解する手助けとなります。

2. インストールとセットアップ

TypeScriptをプロジェクトで使用するためには、まずインストールと設定が必要です。このセクションでは、npmを使ってTypeScriptをインストールする方法、そしてtsconfig.jsonの設定方法を説明します。

2.1 TypeScriptのインストール方法

TypeScriptはnpm(Node Package Manager)を使ってインストールできます。以下の手順で、TypeScriptをプロジェクトにインストールしましょう。

■ 1. npmを使ってインストール

まず、プロジェクトのルートディレクトリに移動し、npmでTypeScriptをインストールします。

npm init -y  # プロジェクトのpackage.jsonを作成(まだ作成していない場合)
npm install typescript --save-dev

これで、プロジェクトのnode_modulesにTypeScriptがインストールされます。--save-dev オプションを使うことで、開発依存としてインストールされ、package.jsondevDependencies に追加されます。

■ 2. インストール確認

インストールが完了したら、以下のコマンドでTypeScriptのバージョンを確認します。

npx tsc --version

これで、インストールされたTypeScriptのバージョンが表示されれば成功です。

2.2 tsconfig.jsonの設定方法

tsconfig.json は、TypeScriptの設定ファイルで、コンパイルオプションやプロジェクトの設定を指定することができます。次に、tsconfig.json をプロジェクトに追加して、TypeScriptの設定を行います。

■ 1. tsconfig.json を自動で生成

TypeScriptをインストールした後、以下のコマンドでtsconfig.jsonを自動生成することができます。

npx tsc --init

このコマンドを実行すると、tsconfig.json がプロジェクトのルートディレクトリに作成されます。

■ 2. 基本的なtsconfig.jsonの設定

生成されたtsconfig.jsonはデフォルトの設定が入っていますが、必要に応じて以下のように設定を変更します。

{
  "compilerOptions": {
    "target": "ES6",                    // 出力するJavaScriptのバージョン
    "module": "commonjs",               // モジュールシステム(Node.jsの場合は"commonjs")
    "strict": true,                     // 厳格な型チェック
    "esModuleInterop": true,            // ESモジュールとCommonJSモジュールの互換性
    "skipLibCheck": true,               // 型定義ファイルのチェックをスキップ
    "forceConsistentCasingInFileNames": true, // ファイル名の大文字小文字を一致させる
    "outDir": "./dist",                 // コンパイル後の出力先ディレクトリ
    "rootDir": "./src"                  // ソースコードのルートディレクトリ
  },
  "include": ["src/**/*.ts"],            // コンパイル対象のファイルを指定
  "exclude": ["node_modules"]            // 除外するファイルやフォルダ
}

この設定では、以下の内容を設定しています:

  • target: 出力されるJavaScriptのバージョンをES6に設定(ES5などに変更も可能)。
  • module: 使用するモジュールシステムをcommonjsに設定(Node.js向け)。
  • strict: 型チェックを厳格に行うオプション(型の安全性を高めます)。
  • outDir: コンパイル後の出力先を./distに指定。
  • rootDir: ソースコードを格納するディレクトリを./srcに指定。

2.3 実際のプロジェクトにTSを導入

TypeScriptを導入するプロジェクトでの一般的なセットアップ手順は以下の通りです。

■ 1. ソースコードのディレクトリ作成

まず、srcというディレクトリを作成し、TypeScriptのソースコードファイル(*.ts)をその中に配置します。

mkdir src

そして、srcディレクトリ内にTypeScriptファイル(例:index.ts)を作成します。

// src/index.ts
const message: string = "Hello, TypeScript!";
console.log(message);

■ 2. TypeScriptのコンパイル

次に、tscコマンドを使ってTypeScriptファイルをコンパイルします。

npx tsc

これにより、tsconfig.jsonに基づいて、srcディレクトリ内のTypeScriptファイルがコンパイルされ、distディレクトリにJavaScriptファイルが出力されます。

■ 3. コンパイル後の実行

コンパイル後、生成されたdist/index.jsを実行します。

node dist/index.js

これで、コンパイル後のJavaScriptが正常に動作することを確認できます。

3. 基本の型システム

TypeScriptでは、変数に型を指定することで、静的型付けのメリットを享受できます。ここでは、基本的な型システムの使い方から、配列型、タプル型、列挙型 (enum)、そして高度な型(型アサーション、リテラル型、ユニオン型)を紹介します。

3.1 基本型の使い方

TypeScriptの基本型には、numberstringbooleanなどがあります。これらはJavaScriptの基本型と同様ですが、TypeScriptでは型を明示的に指定することができます。

number型

number型は数値を表します。

let age: number = 30;
let price: number = 100.5;

string型

string型は文字列を表します。

let name: string = "Alice";
let greeting: string = "Hello, " + name;

boolean型

boolean型は真偽値(trueまたはfalse)を表します。

let isActive: boolean = true;
let hasCompleted: boolean = false;

3.2 配列型やタプル型、列挙型 (enum) の使い方

配列型

TypeScriptでは、配列の型を明示的に指定できます。配列の型は、number[]string[]のように書きます。

let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];

また、Array<number> のようにジェネリクスを使って書くこともできます。

let numbers: Array<number> = [1, 2, 3, 4, 5];

タプル型

タプル型は、異なる型の値を順序通りに格納する配列のようなものです。例えば、[string, number]のように型を指定します。

let person: [string, number] = ["Alice", 30];

タプル型の特徴は、各要素の型が固定されている点です。上記の例では、最初の要素がstring、次がnumberでなければなりません。

列挙型 (enum)

enumは、定義した名前付きの定数セットです。enumを使うことで、定数に名前をつけて使うことができます。

enum Direction {
  Up = 1,
  Down,
  Left,
  Right,
}

let move: Direction = Direction.Up;
console.log(move);  // 1

enumを使うことで、コード内で意味のある名前付きの値を使い、可読性を高めることができます。

3.3 型のアサーションやリテラル型、ユニオン型

型のアサーション

型のアサーションは、変数が特定の型であると「断言」する方法です。asを使って型を指定します。

let value: any = "Hello, TypeScript!";
let length: number = (value as string).length;

上記の例では、valuestring型であることを断言して、その後でlengthプロパティにアクセスしています。

リテラル型

リテラル型は、特定の値そのものを型として指定します。これにより、特定の値だけを許容することができます。

let direction: "up" | "down" = "up";
direction = "down";  // OK
direction = "left";  // エラー: "left" は "up" | "down" の型に割り当てできません。

このように、リテラル型を使うことで、予期しない値が代入されるのを防ぐことができます。

ユニオン型

ユニオン型は、複数の型を組み合わせて、どれか一つの型を受け入れる型です。| を使って複数の型を指定します。

let value: number | string;
value = 42;        // OK
value = "Hello";   // OK
value = true;      // エラー: 'boolean' 型は 'number | string' 型に割り当てできません

ユニオン型を使うことで、変数が複数の型のいずれかを受け入れられるようにすることができます。

4. 関数と型注釈

TypeScriptでは、関数に対して引数や戻り値の型を指定することができます。これにより、関数の挙動が予測しやすくなり、エラーを事前に防ぐことができます。このセクションでは、関数の型注釈の使い方、関数のオーバーロード、可変長引数について説明します。

4.1 関数の型注釈

関数の型注釈を使うことで、引数や戻り値の型を明示的に指定できます。これにより、関数が受け取る値や返す値の型が制限され、型安全が確保されます。

関数の引数と戻り値の型指定

関数の引数や戻り値の型を指定するには、(引数: 型) の形式で書きます。

function add(a: number, b: number): number {
  return a + b;
}

let result = add(5, 10);  // 正常に動作
// let invalidResult = add("5", 10);  // エラー: 'string' 型は 'number' 型に割り当てできません

この例では、ab の引数の型をnumberに指定し、戻り値の型もnumberに指定しています。

戻り値の型注釈

関数が戻り値を返す場合、その型も指定できます。例えば、文字列を返す関数の例です。

function greet(name: string): string {
  return "Hello, " + name;
}

let greeting = greet("Alice");  // "Hello, Alice"

4.2 関数のオーバーロード

TypeScriptでは、関数のオーバーロードを使用して、同じ名前の関数に異なる引数の型や戻り値の型を設定できます。オーバーロードを使うことで、関数が異なる引数に対して異なる動作をすることができます。

オーバーロードの定義

オーバーロードを定義するには、関数のシグネチャ(型定義)を複数回記述し、最後に実際の関数の実装を記述します。

function greet(name: string): string;
function greet(age: number): string;
function greet(value: string | number): string {
  if (typeof value === "string") {
    return "Hello, " + value;
  } else {
    return "You are " + value + " years old.";
  }
}

let greeting1 = greet("Alice");  // "Hello, Alice"
let greeting2 = greet(25);       // "You are 25 years old."

この例では、greet 関数が文字列を受け取った場合は「Hello, [name]」、数字を受け取った場合は「You are [age] years old.」というメッセージを返します。関数シグネチャを複数定義することで、異なる型に対応できます。

4.3 可変長引数の型指定

可変長引数(Rest Parameters)は、関数が任意の数の引数を受け取れるようにする方法です。TypeScriptでは、...(スプレッド演算子)を使って可変長引数を指定できます。

可変長引数の型指定

可変長引数に型を指定するには、...引数名: 型[]の形式で書きます。

function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}

let result = sum(1, 2, 3, 4, 5);  // 15

この例では、numbersnumber[]型の可変長引数であり、sum関数はその引数の合計を計算します。

可変長引数の型と型推論

TypeScriptは可変長引数の型を自動的に推論しますが、明示的に型を指定することもできます。例えば、string型の可変長引数を受け取る関数は次のように定義できます。

function concatenate(...strings: string[]): string {
  return strings.join(" ");
}

let sentence = concatenate("Hello", "TypeScript", "world!");  // "Hello TypeScript world!"

この関数は、string型の任意の数の引数を受け取り、それらをスペースで結合して返します。


このように、関数の型注釈を使うことで、引数や戻り値の型を安全に管理し、関数の挙動を明確にすることができます。また、関数のオーバーロードや可変長引数を使用することで、柔軟な関数の設計が可能になります。

5. オブジェクト型とインターフェース

TypeScriptでは、オブジェクト型を使って複数の値を1つのまとまりとして扱うことができます。また、interface を使うことで、より強力に型定義を行うことができます。このセクションでは、オブジェクト型の定義方法と、interface を使った型定義について説明します。

5.1 オブジェクト型の定義方法

オブジェクト型は、{}(中括弧)を使って定義します。オブジェクト型には、オブジェクト内のプロパティの型を指定することができます。

オブジェクト型の基本例

let person: { name: string, age: number } = {
  name: "Alice",
  age: 30,
};

この例では、personという変数が、name(文字列)とage(数値)を持つオブジェクトであることを型注釈で指定しています。

オプショナルなプロパティ

オブジェクトのプロパティが必須でなく、任意である場合には、プロパティ名の後ろに?を付けてオプショナルにすることができます。

let person: { name: string, age?: number } = {
  name: "Alice",
};

person.age = 30;  // 任意のプロパティなので、後から追加可能

ageプロパティはオプショナルなので、オブジェクト作成時に省略することも可能です。

インデックス型

インデックス型を使って、プロパティ名が動的に決まるオブジェクトを型定義することもできます。インデックス型は、プロパティ名が特定の型で、プロパティの値が別の型である場合に使用します。

let scores: { [subject: string]: number } = {
  math: 95,
  english: 88,
};

この場合、scoresは任意の文字列をプロパティ名として、数値をプロパティ値として持つオブジェクトです。

5.2 interface を使って型を定義する方法

interfaceは、オブジェクト型の定義をより強力にするための構文です。interfaceを使うことで、型の再利用や拡張が容易になり、クラスとの統合が簡単になります。

interface の基本例

interfaceを使って型を定義する例です。

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

let person: Person = {
  name: "Alice",
  age: 30,
};

ここで、Personというinterfaceを定義し、そのinterfaceを使ってpersonの型を指定しています。これにより、personオブジェクトはnameageプロパティを持つことが保証されます。

オプショナルなプロパティ

interfaceでもオプショナルなプロパティを定義できます。プロパティ名の後ろに?を付けます。

interface Person {
  name: string;
  age?: number;  // オプショナル
}

let person1: Person = { name: "Alice" };
let person2: Person = { name: "Bob", age: 25 };

person1ageプロパティを省略できますが、person2ageを含むことができます。

関数型のinterface

interfaceを使って関数の型を定義することもできます。

interface Greet {
  (name: string): string;
}

let greet: Greet = function (name: string): string {
  return "Hello, " + name;
};

console.log(greet("Alice"));  // "Hello, Alice"

この例では、Greetというinterfaceが、name: stringを引数に取り、stringを返す関数型を定義しています。

5.3 クラスとの組み合わせ

interfaceは、クラスが実装することもできます。クラスがinterfaceを実装することで、そのクラスが必要なプロパティやメソッドを持っていることが保証されます。

クラスでinterfaceを実装

interface Person {
  name: string;
  age: number;
  greet(): void;
}

class Employee implements Person {
  constructor(public name: string, public age: number) {}

  greet(): void {
    console.log("Hello, my name is " + this.name);
  }
}

let employee = new Employee("Alice", 30);
employee.greet();  // "Hello, my name is Alice"

この例では、EmployeeクラスがPersonインターフェースを実装しています。Employeeクラスはnameagegreetメソッドを持つことが保証されます。

interfaceの拡張

interfaceは他のinterfaceを継承(拡張)することができます。これにより、複数のインターフェースの機能を組み合わせて使用することができます。

interface Animal {
  name: string;
  sound(): void;
}

interface Dog extends Animal {
  breed: string;
}

let dog: Dog = {
  name: "Buddy",
  breed: "Golden Retriever",
  sound() {
    console.log("Woof");
  },
};

console.log(dog.name);  // "Buddy"
console.log(dog.breed); // "Golden Retriever"
dog.sound();            // "Woof"

この例では、DogインターフェースがAnimalインターフェースを拡張し、Dogに特有のプロパティbreedを追加しています。


TypeScriptのinterfaceは非常に強力で、型を拡張したり、クラスと組み合わせて使用することで、より堅牢で保守性の高いコードを実現できます。

6. クラスと継承

TypeScriptは、オブジェクト指向プログラミングの概念をサポートしており、クラスと継承を使って、より構造的なコードを書くことができます。このセクションでは、クラスの定義方法、継承の仕組み、abstract classreadonly プロパティの使い方について説明します。

6.1 クラスの定義と型安全な継承

TypeScriptでクラスを定義するには、classキーワードを使用します。クラスは、プロパティやメソッドを持つオブジェクトの設計図として機能します。

クラスの基本的な定義

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet(): void {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

let person = new Person("Alice", 30);
person.greet();  // "Hello, my name is Alice."

この例では、Personクラスがnameageというプロパティと、greetというメソッドを持っています。constructorメソッドでクラスインスタンスを初期化します。

型安全な継承

TypeScriptでは、クラスは他のクラスを継承することができます。継承を使うことで、基底クラス(親クラス)のプロパティやメソッドを派生クラス(子クラス)で再利用できます。

class Employee extends Person {
  position: string;

  constructor(name: string, age: number, position: string) {
    super(name, age);  // 親クラスのコンストラクタを呼び出す
    this.position = position;
  }

  greet(): void {
    super.greet();  // 親クラスのgreetメソッドを呼び出す
    console.log(`I am a(n) ${this.position}.`);
  }
}

let employee = new Employee("Bob", 40, "Developer");
employee.greet();
// "Hello, my name is Bob."
// "I am a(n) Developer."

この例では、EmployeeクラスがPersonクラスを継承しています。super()を使って、親クラスのコンストラクタを呼び出し、親クラスのプロパティも引き継ぎます。また、greetメソッドをオーバーライドして、親クラスのgreetメソッドも呼び出しています。

6.2 abstract class の使い方

abstract classは、直接インスタンス化できないクラスで、サブクラスによって実装されるべきメソッドを定義します。abstractメソッドは、子クラスで実装しなければなりません。

abstract class の基本例

abstract class Animal {
  abstract sound(): void;  // サブクラスで実装する必要がある

  move(): void {
    console.log("The animal moves.");
  }
}

class Dog extends Animal {
  sound(): void {
    console.log("Woof!");
  }
}

let dog = new Dog();
dog.sound();  // "Woof!"
dog.move();   // "The animal moves."

この例では、Animalクラスがabstractとして定義され、soundメソッドは抽象メソッドとして親クラスに定義されています。Dogクラスでsoundメソッドを実装することで、Animalクラスを継承した具体的なクラスを作成できます。

abstract class の使いどころ

abstract classは、共通の基底機能を持ちながら、特定の実装をサブクラスに任せたい場合に便利です。例えば、Animalクラスで動物の共通の動作(moveメソッド)を定義し、soundメソッドの実装は動物ごとに異なるため、サブクラスで実装させています。

6.3 readonly プロパティの使い方

readonlyは、オブジェクトのプロパティが初期化後に変更されないことを保証するための修飾子です。readonlyを付けると、そのプロパティは変更不可能になります。

readonly プロパティの基本例

class Car {
  readonly brand: string;

  constructor(brand: string) {
    this.brand = brand;
  }
}

let myCar = new Car("Toyota");
console.log(myCar.brand);  // "Toyota"

// myCar.brand = "Honda";  // エラー: 'brand' は 'readonly' プロパティです

readonlyを使用することで、クラスのプロパティが不変であることを保証できます。これにより、意図しない変更を防ぎ、コードの信頼性が向上します。

6.4 クラスのアクセス修飾子

TypeScriptでは、クラスのプロパティやメソッドにアクセス修飾子(publicprivateprotected)を使用することができます。これにより、どこからアクセスできるかを制御できます。

  • public: クラス外からもアクセスできる(デフォルト)。
  • private: クラス内からのみアクセスできる。
  • protected: クラス内およびサブクラスからアクセスできる。
class Person {
  public name: string;
  private age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  getAge(): number {
    return this.age;  // privateプロパティへのアクセス
  }
}

let person = new Person("Alice", 30);
console.log(person.name);  // "Alice"
console.log(person.getAge());  // 30
// console.log(person.age);  // エラー: 'age' は 'private' プロパティです

この例では、nameプロパティはpublic(デフォルト)で、ageプロパティはprivateとして定義されています。privateなプロパティにはクラス外からアクセスできませんが、getAgeメソッドを通じてageにアクセスできます。


TypeScriptのクラスと継承を活用することで、オブジェクト指向プログラミングをより安全に実践できます。abstract classreadonlyプロパティなどの機能を使うことで、より堅牢で保守性の高いコードを書くことができます。

7. 型推論と型ガード

TypeScriptは、型安全を強化するだけでなく、型推論や型ガードを使って、コードの可読性と保守性を高めることができます。型推論は、明示的に型を指定しなくても、TypeScriptが変数の型を推測してくれる機能です。型ガードを使うことで、実行時に型を安全に絞り込むことができます。

7.1 型推論

型推論とは、変数や関数の戻り値などに対して、TypeScriptが自動的に型を推測する機能です。これにより、コードの冗長性が減り、型を明示的に指定しなくても、型安全が保たれます。

型推論の基本例

例えば、次のコードでは、number型を指定せずに変数ageに値を代入していますが、TypeScriptはその値から型を推論します。

let age = 30;  // TypeScriptは自動的に 'age' を number 型として推論します
console.log(age);  // 30

この場合、ageは自動的にnumber型として推論されます。TypeScriptが自動で型を推測するため、特に型指定をしなくても安全に動作します。

型推論が働く例

TypeScriptは、関数の戻り値やオブジェクトのプロパティなどにも型推論を適用します。

function add(a: number, b: number) {
  return a + b;  // TypeScriptは戻り値を number 型として推論します
}

let result = add(10, 20);
console.log(result);  // 30

この関数addの戻り値も、TypeScriptはnumber型と推論します。関数の戻り値の型を明示的に指定しなくても、TypeScriptはその値を型推論で自動的に判定します。

型推論と型アサーション

もし、TypeScriptの型推論が誤って推測した場合や、明示的に型を指定したい場合には型アサーションを使って型を指定することができます。

let value: any = "Hello, TypeScript!";
let length: number = (value as string).length;  // 'value' を string 型として扱う

このように、asを使ってvaluestring型としてアサートすることで、型推論が誤っている場合でも、型を強制的に指定できます。

7.2 型ガード

型ガードは、実行時に変数やオブジェクトの型を確認し、その型に基づいて安全に処理を行う方法です。型ガードを使用することで、TypeScriptは特定の型を確実に扱うことができ、実行時エラーを防げます。

typeof による型ガード

typeof演算子は、プリミティブ型(number, string, boolean など)の型チェックに使われます。

function isNumber(value: any): boolean {
  if (typeof value === "number") {
    return true;  // valueはnumber型
  }
  return false;
}

let num = 10;
if (isNumber(num)) {
  console.log(num.toFixed(2));  // 型が安全にnumberとして扱える
}

この例では、typeof演算子を使って、valuenumber型かどうかを判定し、number型であればその後の処理でtoFixed()メソッドを安全に呼び出しています。

instanceof による型ガード

instanceofは、オブジェクトが特定のクラスまたはコンストラクタ関数のインスタンスかどうかを確認するために使用します。

class Dog {
  bark(): void {
    console.log("Woof!");
  }
}

class Cat {
  meow(): void {
    console.log("Meow!");
  }
}

function speak(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark();  // animalはDog型として扱える
  } else if (animal instanceof Cat) {
    animal.meow();  // animalはCat型として扱える
  }
}

let dog = new Dog();
let cat = new Cat();

speak(dog);  // "Woof!"
speak(cat);  // "Meow!"

この例では、instanceofを使って、animalDogCatかを確認し、それに応じたメソッドを呼び出しています。これにより、実行時に型が確実に絞り込まれ、型安全なコードを実現できます。

in 演算子による型ガード

in演算子を使うと、オブジェクトが特定のプロパティを持っているかどうかを確認できます。

interface Dog {
  bark(): void;
}

interface Cat {
  meow(): void;
}

function speak(animal: Dog | Cat) {
  if ("bark" in animal) {
    animal.bark();  // animalはDog型として扱える
  } else {
    animal.meow();  // animalはCat型として扱える
  }
}

let dog: Dog = { bark: () => console.log("Woof!") };
let cat: Cat = { meow: () => console.log("Meow!") };

speak(dog);  // "Woof!"
speak(cat);  // "Meow!"

この例では、in演算子を使って、animalbarkメソッドを持っているかどうかを確認し、その型に応じたメソッドを呼び出しています。


7.3 型推論と型ガードのまとめ

  • 型推論: TypeScriptは、変数や関数の戻り値を自動的に推測して型を付与します。明示的に型を指定しなくても、安全にコードを書くことができます。
  • 型ガード: 実行時に型を絞り込むために、typeofinstanceofinなどの型ガードを使うことで、安全に型を扱えます。

これらの機能を活用することで、より型安全で堅牢なコードを書くことができ、エラーを事前に防ぐことができます。

8. 非同期処理とPromise

非同期処理は、JavaScriptで多くのアプリケーションで使用される重要な概念です。TypeScriptでは、非同期処理において型安全を保ちながら、async / awaitPromiseを使用することができます。このセクションでは、非同期処理を扱う際の型注釈や、Promise の型について解説します。

8.1 async / await で扱う際の型注釈

async / awaitを使うことで、非同期処理を同期的に記述することができ、コードが読みやすくなります。TypeScriptでasync / awaitを使用する際には、戻り値に対して型注釈を行うことで、型安全を保つことができます。

async 関数の型注釈

async関数は常にPromiseを返します。したがって、戻り値の型はPromise<型>の形式になります。以下の例では、非同期関数がstring型の結果をPromiseとして返すことを示しています。

async function fetchData(): Promise<string> {
  return "Data fetched successfully!";
}

fetchData().then((result) => {
  console.log(result);  // "Data fetched successfully!"
});

この例では、fetchData関数がPromise<string>を返すことを明示的に型注釈しています。

async / await の使用

async 関数内で非同期処理をawaitを使って同期的に書くことができます。非同期の操作が完了するまで待機し、結果を返す形になります。

async function getUserData(userId: number): Promise<{ id: number, name: string }> {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  const user = await response.json();
  return user;
}

getUserData(1).then(user => {
  console.log(user);  // { id: 1, name: 'Alice' }
});

この例では、getUserData 関数が Promise<{ id: number, name: string }> 型を返します。awaitを使用することで、fetch APIの非同期結果を同期的に処理しています。

8.2 Promise の型の使い方

Promise は非同期処理の結果を表すオブジェクトです。Promiseを使うと、非同期処理が成功した場合に結果の値を取得し、失敗した場合にはエラーを捕捉することができます。TypeScriptでは、Promiseの型を指定することで、非同期処理の結果の型を明確にすることができます。

Promiseの型指定

Promiseの型はPromise<型>の形式で指定します。例えば、Promise<string>は文字列を返す非同期処理を意味します。

let fetchString: Promise<string> = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("Hello, World!");
  }, 1000);
});

fetchString.then(result => {
  console.log(result);  // "Hello, World!"
});

この例では、fetchStringPromise<string>型で、非同期的に文字列を返すPromiseです。

Promise のエラーハンドリング

Promiseには、成功した場合のthenメソッドと、失敗した場合のcatchメソッドを使ってエラーハンドリングを行います。

let fetchNumber: Promise<number> = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error("Something went wrong"));
  }, 1000);
});

fetchNumber
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error(error);  // "Error: Something went wrong"
  });

この場合、非同期処理が失敗した場合には、catchメソッドでエラーを処理できます。

Promise を返す関数の型注釈

Promiseを返す関数の型注釈は、次のように記述します。

function delay(ms: number): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

delay(1000).then(() => {
  console.log("1 second passed");
});

delay関数はPromise<void>を返します。これは、Promiseの結果が返されない(void型)ことを示しています。

8.3 async / await と Promise の組み合わせ

async / awaitを使うことで、Promiseをより簡潔に扱うことができます。awaitを使うと、Promiseが解決するまで処理を待機し、結果を返します。

async / await とPromise の使い方

async function fetchData(): Promise<string> {
  const data: string = await new Promise((resolve) => {
    setTimeout(() => {
      resolve("Fetched data successfully");
    }, 1000);
  });
  return data;
}

fetchData().then((data) => {
  console.log(data);  // "Fetched data successfully"
});

この例では、fetchData関数内でPromiseawaitし、非同期処理の結果を同期的に取得しています。


8.4 Promise と非同期処理の型安全性

TypeScriptでは、非同期処理に対して型注釈を行うことで、処理が完了する前に結果の型にアクセスすることを防ぎます。また、async / awaitを使うことで、非同期処理をより直感的に書けるようになります。

Promiseの型注釈を活用し、型安全な非同期処理を実現することで、実行時エラーを未然に防ぐことができます。


このように、async / awaitPromiseを組み合わせて、非同期処理を型安全に扱うことができます。

9. ジェネリクス

ジェネリクスは、TypeScriptの強力な機能で、型をパラメータ化して、コードの再利用性を高めることができます。関数やクラス、インターフェースでのジェネリクスの使い方を学び、柔軟で型安全なコードを作成しましょう。

9.1 ジェネリクスを使って再利用性の高い型を定義する方法

ジェネリクスは、型を引数として受け取ることができるため、関数やクラス、インターフェースなどで再利用可能な型を定義することができます。型パラメータは、<T> のように定義します。

基本的なジェネリクスの使い方

ジェネリクスを使うことで、型を後から指定できる汎用的な関数を作成できます。

function identity<T>(arg: T): T {
  return arg;
}

let num = identity(5);  // numは 'number' 型
let str = identity("hello");  // strは 'string' 型

このidentity関数は、引数argに渡された型Tをそのまま返す汎用的な関数です。Tは型パラメータであり、呼び出し時に具体的な型(numberstringなど)を指定します。

ジェネリクスの型推論

ジェネリクスを使うと、TypeScriptが型を自動的に推論してくれる場合もあります。

let numberValue = identity(42);  // TypeScriptはnumber型を推論
let stringValue = identity("TypeScript");  // TypeScriptはstring型を推論

このように、型を明示的に指定しなくても、関数の引数の型からTypeScriptが型を推論してくれます。

9.2 関数でのジェネリクスの使い方

関数でジェネリクスを使用することで、異なる型の値を受け入れ、返すことができます。これにより、関数がどの型にも対応できるようになり、再利用性が向上します。

複数のジェネリクスパラメータ

関数が複数の異なる型を扱う場合、複数のジェネリクスパラメータを使うことができます。

function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

let merged = merge({ name: "Alice" }, { age: 30 });
console.log(merged);  // { name: 'Alice', age: 30 }

この例では、merge関数は、T型とU型の2つの引数を受け取り、両方の型を持つオブジェクトを返します。T & Uは、TUの型を両方持つ新しい型を作成しています。

9.3 クラスでのジェネリクスの使い方

クラスでジェネリクスを使うことで、型安全で柔軟なクラスを作成することができます。ジェネリクスをクラスのプロパティやメソッドに適用することで、異なる型に対応したインスタンスを作成できます。

クラスでのジェネリクスの基本

class Box<T> {
  value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }
}

let numberBox = new Box<number>(10);
let stringBox = new Box<string>("Hello");

console.log(numberBox.getValue());  // 10
console.log(stringBox.getValue());  // "Hello"

この例では、BoxクラスがジェネリクスTを使って、異なる型のvalueを持つインスタンスを作成しています。numberBoxnumber型、stringBoxstring型の値を保持します。

クラスの継承とジェネリクス

ジェネリクスは、クラスの継承にも対応しています。サブクラスで親クラスのジェネリクスを利用して型を継承できます。

class Container<T> {
  value: T;

  constructor(value: T) {
    this.value = value;
  }
}

class StringContainer extends Container<string> {
  getLength(): number {
    return this.value.length;
  }
}

let stringContainer = new StringContainer("Hello");
console.log(stringContainer.getLength());  // 5

この例では、Containerクラスをジェネリクスで定義し、StringContainerクラスでstring型を継承しています。

9.4 ジェネリクスの制約

ジェネリクスに制約を付けることで、特定の型に制限することができます。制約を使うことで、ジェネリクスに許可される型を限定し、型安全を保ちます。

ジェネリクスに制約を付ける

function getLength<T extends { length: number }>(value: T): number {
  return value.length;
}

console.log(getLength("Hello"));  // 5
console.log(getLength([1, 2, 3]));  // 3
// console.log(getLength(123));  // エラー: 'number' 型は 'length' プロパティを持っていません

この例では、ジェネリクスTlengthプロパティを持つ型のみを受け入れる制約を付けています。そのため、stringarrayなどのlengthプロパティを持つ型のみが許可されます。


9.5 ジェネリクスのまとめ

  • ジェネリクスを使うことで、関数やクラスが型に依存せずに汎用的に動作し、再利用性が高いコードを作成できます。
  • 複数のジェネリクスパラメータを使って、異なる型を組み合わせた処理を柔軟に記述できます。
  • クラスでのジェネリクスを使うことで、インスタンスの型をパラメータ化し、異なる型を扱うことができます。
  • ジェネリクスに制約を付けることで、型安全を高め、指定された型だけを許可することができます。

これらのジェネリクスの機能を使いこなすことで、型の柔軟性を保ちながら、強力で再利用可能なコードを作成できます。

10. 高度な型システム

TypeScriptの型システムは非常に柔軟で強力です。基本的な型システムに加えて、さらに高度な型の使い方を知ることで、より複雑で堅牢なコードを作成することができます。このセクションでは、条件付き型、交差型、マップ型、インデックス型など、TypeScriptの高度な型システムを紹介します。

10.1 条件付き型(Conditional Types)

条件付き型は、型が特定の条件を満たす場合に異なる型を返すことができる型の一種です。型の推論を行い、その結果に基づいて型を決定します。

基本的な条件付き型の使い方

条件付き型は、T extends U ? X : Y の形式で記述します。TUを拡張していれば型Xを返し、そうでなければ型Yを返します。

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

let result1: IsString<string> = "Yes";  // "Yes"
let result2: IsString<number> = "No";   // "No"

この例では、IsString型は、型Tstring型であれば"Yes"、それ以外なら"No"を返すようになっています。

条件付き型を使った実用例

条件付き型を使うと、より複雑な型の操作が可能になります。例えば、nullundefinedを除外する型を定義できます。

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

let value: NotNull<string | null> = "Hello";  // OK
// let invalidValue: NotNull<string | null> = null;  // エラー: 'null' は 'never' 型に割り当てできません

この例では、NotNull型を使って、nullundefinedを除外した型を定義しています。

10.2 交差型(Intersection Types)

交差型は、複数の型を組み合わせて1つの型にすることができる型です。複数の型を&演算子で結合します。これにより、両方の型の特性を持った型を作成できます。

交差型の基本的な使い方

type Person = { name: string };
type Employee = { employeeId: number };

type EmployeeDetails = Person & Employee;

let employee: EmployeeDetails = {
  name: "Alice",
  employeeId: 123,
};

この例では、PersonEmployeeの2つの型を交差型で組み合わせたEmployeeDetails型を定義しています。EmployeeDetails型は、PersonEmployeeの両方のプロパティを持つことになります。

交差型を使った実用例

交差型を使うことで、複数の異なる型を組み合わせたオブジェクトを作成できます。

type Address = { street: string, city: string };
type ContactInfo = { email: string, phone: string };

type User = Address & ContactInfo;

let user: User = {
  street: "123 Main St",
  city: "New York",
  email: "alice@example.com",
  phone: "123-456-7890",
};

User型は、Address型とContactInfo型の両方のプロパティを持つことになります。

10.3 マップ型(Mapped Types)

マップ型は、既存の型を元にして新しい型を作成するための型です。特に、オブジェクトのプロパティを一括で変換したい場合に便利です。keyofinを組み合わせて使います。

マップ型の基本的な使い方

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

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

let person: ReadOnlyPerson = { name: "Alice", age: 30 };
// person.name = "Bob";  // エラー: 'name' は読み取り専用プロパティです

この例では、Person型のすべてのプロパティをreadonlyにした新しい型ReadOnlyPersonを作成しています。

マップ型を使ってプロパティ名を変換

マップ型を使うと、オブジェクトのプロパティを一括で変換することもできます。例えば、プロパティ名をすべてstring型に変換することができます。

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

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

let person: StringifiedPerson = { name: "Alice", age: "30" };

この例では、Person型のすべてのプロパティをstring型に変換したStringifiedPerson型を作成しています。

10.4 インデックス型(Index Types)

インデックス型は、オブジェクトのプロパティ名を使って型を指定する方法です。特定の型のキーに対して、プロパティの型を動的に指定できます。

インデックス型の基本的な使い方

type Dictionary<T> = {
  [key: string]: T;
};

let numberDict: Dictionary<number> = {
  age: 30,
  score: 95,
};

let stringDict: Dictionary<string> = {
  name: "Alice",
  country: "USA",
};

この例では、Dictionary型を定義し、string型のキーに対して、値を任意の型(T)として指定しています。これにより、Dictionary型は様々な型の値を持つオブジェクトとして使えます。


10.5 高度な型システムのまとめ

  • 条件付き型は、型の条件に応じて異なる型を返すことができます。これにより、柔軟で強力な型システムを作成できます。
  • 交差型は、複数の型を結合して、新しい型を作ることができます。これにより、型の拡張や再利用が容易になります。
  • マップ型は、オブジェクトのプロパティを一括で変換することができ、プロパティの変更や再利用を効率的に行えます。
  • インデックス型は、動的にプロパティ名に基づいて型を指定する方法です。プロパティ名が不確定な場合にも対応できます。

これらの高度な型システムを使いこなすことで、TypeScriptの型安全性を高め、より堅牢でメンテナンスしやすいコードを書くことができます。

TypeScriptとReact(オプション)

TypeScriptとReactを組み合わせることで、コンポーネントの型安全性を高め、開発時にエラーを早期に検出することができます。このセクションでは、ReactでTypeScriptを使用する際の基本的な型付け方法を解説します。

1. ReactでTypeScriptを使う際の基本的な型付け方法

ReactコンポーネントにTypeScriptを導入する場合、React.FC(Functional Component)やReact.Componentの型を使って、関数型コンポーネントやクラス型コンポーネントを型安全に定義できます。

関数型コンポーネントの型付け

Reactでの関数型コンポーネントは、React.FCを使って型を付けることができます。React.FCは、コンポーネントの戻り値をJSX.Elementとして推論します。

import React from 'react';

interface GreetingProps {
  name: string;
}

const Greeting: React.FC<GreetingProps> = ({ name }) => {
  return <h1>Hello, {name}!</h1>;
};

export default Greeting;

この例では、GreetingコンポーネントがGreetingProps型を受け取り、nameを文字列として受け取ることを保証しています。React.FC<GreetingProps>により、コンポーネントがpropsを正しく受け取ることが型安全に保証されます。

注意点

React.FCは型安全を高めますが、childrenが暗黙的に型に含まれるため、特にchildrenを使わない場合でも不要な型付けがされてしまうことがあります。最近では、React.FCを使わずに、直接propsの型を指定する方法が推奨されています。

2. PropsやStateの型付け

Reactのコンポーネントでは、propsstateの型付けも重要です。TypeScriptを使用することで、コンポーネントのpropsstateに型を安全に指定できます。

Propsの型付け

propsはコンポーネントの外部から渡されるデータです。propsの型を定義するために、interfacetypeを使って型を指定します。

import React from 'react';

interface CounterProps {
  initialCount: number;
}

const Counter: React.FC<CounterProps> = ({ initialCount }) => {
  const [count, setCount] = React.useState(initialCount);

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default Counter;

この例では、CounterPropsインターフェースでinitialCountの型をnumberとして指定し、コンポーネントでそのpropsを受け取っています。

Stateの型付け

useStateフックでは、stateに型を指定することができます。useStateに型引数を渡すことで、stateの型を明確にすることができます。

import React from 'react';

const Counter: React.FC = () => {
  const [count, setCount] = React.useState<number>(0); // `count`の型を指定

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default Counter;

ここでは、useState<number>(0)と記述することで、countの型がnumberであることを明示的に指定しています。

3. イベントハンドラーの型指定

Reactのイベントハンドラーにも型を指定できます。特に、イベントオブジェクトの型を明確にすることが重要です。Reactでは、React.MouseEventReact.ChangeEventなどの型が用意されています。

マウスイベントの型付け

import React from 'react';

const Button: React.FC = () => {
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    console.log("Button clicked!", event);
  };

  return <button onClick={handleClick}>Click me</button>;
};

export default Button;

ここでは、handleClick関数の引数としてReact.MouseEvent<HTMLButtonElement>を指定することで、マウスクリックイベントがHTMLButtonElementに関連することを明示的に型付けしています。

入力イベントの型付け

inputタグなどのフォーム要素では、onChangeイベントがよく使われます。その際、React.ChangeEvent<HTMLInputElement>を使用して型を指定できます。

import React from 'react';

const TextInput: React.FC = () => {
  const [value, setValue] = React.useState<string>("");

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setValue(event.target.value);
  };

  return <input type="text" value={value} onChange={handleChange} />;
};

export default TextInput;

この例では、handleChangeの引数としてReact.ChangeEvent<HTMLInputElement>を指定し、input要素のonChangeイベントを正しく型付けしています。


まとめ

  • Propsの型付け: interfacetypeを使って、コンポーネントのpropsに型を指定します。
  • Stateの型付け: useStateフックを使用して、stateの型を指定できます。
  • イベントハンドラーの型付け: React.MouseEventReact.ChangeEventを使って、イベントハンドラーに型を指定します。

TypeScriptとReactを組み合わせることで、型安全なコードを書き、開発時にエラーを未然に防ぐことができます。Reactコンポーネントでの型付けを適切に行うことで、コードの品質が向上し、保守性が高まります。