CodeKitchen

TypeScript初学者が中級者レベルに駆け上がるための完全ガイド

typescript

1. TypeScriptの概要

TypeScriptは、Microsoftによって開発されたオープンソースのプログラミング言語であり、JavaScriptに静的型付けを追加した上位互換言語です。TypeScriptを使用することで、コンパイル時にエラーを検出し、より堅牢で保守性の高いコードを書くことができます。

1.1. TypeScriptとは

TypeScriptは、JavaScriptに以下のような機能を追加しています:

  • 静的型付け
  • クラスベースのオブジェクト指向プログラミング
  • ジェネリック
  • モジュールシステム
  • 型推論
  • 型定義ファイル(.d.ts)

TypeScriptで書かれたコードは、TypeScriptコンパイラ(tsc)によってJavaScriptに変換されます。生成されたJavaScriptコードは、ブラウザやNode.jsなどの任意のJavaScript実行環境で動作します。

1.2. TypeScriptの特徴と利点

TypeScriptの主な特徴と利点は以下の通りです:

  1. 静的型付け:変数、関数の引数、返り値などに型を指定することで、コンパイル時にエラーを検出できます。これにより、実行時のエラーを防ぎ、コードの質を向上させることができます。

  2. 強力な型推論:型を明示的に指定しない場合でも、TypeScriptは変数の初期化子や関数の返り値から型を推論します。これにより、冗長な型アノテーションを減らし、コードの可読性を高めることができます。

  3. オブジェクト指向プログラミング:TypeScriptは、クラス、インターフェース、継承、モジュールなどの機能を提供し、オブジェクト指向プログラミングをサポートします。これにより、コードの再利用性や保守性を高めることができます。

  4. 豊富なツールサポート:TypeScriptは、Visual Studio Code、WebStorm、Sublimeテキストなど、多くの人気のあるIDEやエディタでサポートされています。これらのツールは、コード補完、型チェック、リファクタリングなどの機能を提供し、開発者の生産性を向上させます。

  5. 大規模なコミュニティとエコシステム:TypeScriptは、大規模なオープンソースコミュニティによって支えられており、数多くのライブラリやフレームワークがTypeScriptをサポートしています。これにより、開発者は豊富なリソースやツールを利用してアプリケーションを構築することができます。

1.3. TypeScriptのコンパイル方法

TypeScriptのコードをJavaScriptに変換するには、TypeScriptコンパイラ(tsc)を使用します。tscは、Node.jsのnpmパッケージマネージャを使ってインストールできます。

npm install -g typescript

TypeScriptファイル(.tsファイル)をコンパイルするには、以下のコマンドを実行します:

tsc app.ts

このコマンドにより、app.tsファイルがコンパイルされ、app.jsファイルが生成されます。

TypeScriptプロジェクトの設定は、tsconfig.jsonファイルで行います。このファイルでは、コンパイルオプション、ファイルの取り込みと除外、プロジェクトリファレンスなどを指定できます。

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "dist"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}

上記の例では、ECMAScript 6を出力ターゲットとし、CommonJSモジュールシステムを使用しています。また、厳密な型チェックを有効にし、ES モジュールと CommonJS モジュールの相互運用性を向上させています。コンパイル後のファイルは、distディレクトリに出力されます。

includeexcludeオプションを使って、コンパイル対象のファイルを指定することもできます。上記の例では、srcディレクトリ以下のすべてのファイルをコンパイル対象とし、node_modulesディレクトリと.spec.tsファイルを除外しています。

これで、TypeScriptプロジェクトの基本的な設定が完了です。次の章では、TypeScriptの型システムについて詳しく説明します。

2. 型システムの基礎

TypeScriptの型システムは、変数、関数、オブジェクトなどに型を割り当てることで、コードの正確性を高め、エラーを防ぐことができます。この章では、TypeScriptの基本的な型について説明します。

2.1. プリミティブ型

TypeScriptには、以下のプリミティブ型があります:

  • number:整数や浮動小数点数を表します。
  • string:テキストデータを表します。
  • booleantrueまたはfalseの値を表します。
  • null:値が存在しないことを表します。
  • undefined:値が割り当てられていないことを表します。

これらの型は、次のように変数に割り当てることができます:

let age: number = 25;
let name: string = "John";
let isStudent: boolean = true;
let value: null = null;
let unassigned: undefined = undefined;

2.2. オブジェクト型

オブジェクト型は、プロパティの集合を表します。オブジェクト型は、次のように定義できます:

let person: { name: string; age: number } = {
  name: "John",
  age: 25,
};

このようにオブジェクト型を定義することで、オブジェクトのプロパティに型を指定できます。

2.3. 配列型

配列型は、同じ型の値の集合を表します。配列型は、type[]のように記述します:

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

配列型は、ジェネリック型Array<type>を使って記述することもできます:

let numbers: Array<number> = [1, 2, 3, 4, 5];
let names: Array<string> = ["John", "Jane", "Alice"];

2.4. タプル型

タプル型は、固定長の配列で、各要素の型が指定されています。タプル型は、次のように定義します:

let person: [string, number] = ["John", 25];

この例では、personタプルの最初の要素はstring型、2番目の要素はnumber型であると指定しています。

2.5. エニュメレーション型

エニュメレーション型(enum型)は、名前付きの定数の集合を定義するために使用します。enum型は、次のように定義します:

enum Color {
  Red,
  Green,
  Blue,
}

let favoriteColor: Color = Color.Blue;

この例では、Colorという名前のenum型を定義し、RedGreenBlueという3つの定数を持っています。enum型の値は、デフォルトで0から始まる数値になります(Red = 0Green = 1Blue = 2)。

2.6. any型とunknown型

any型は、任意の型の値を保持できる特殊な型です。any型を使用すると、型チェックが行われなくなります。any型は、次のように使用します:

let value: any = 10;
value = "hello";
value = true;

unknown型は、any型と似ていますが、unknown型の値を使用するには、型アサーションや型ガードを使って型を絞り込む必要があります。

let value: unknown = 10;
console.log(value.toFixed(2)); // Error: Object is of type 'unknown'.

if (typeof value === "number") {
  console.log(value.toFixed(2)); // OK
}

2.7. void型とnever型

void型は、関数が値を返さないことを示すために使用します。

function sayHello(): void {
  console.log("Hello!");
}

never型は、決して発生しない値の型を表します。例えば、常に例外をスローする関数や、無限ループが含まれる関数の返り値の型として使用されます。

function throwError(message: string): never {
  throw new Error(message);
}

これで、TypeScriptの基本的な型について説明が終わりました。次の章では、関数の型付けについて詳しく説明します。

3. 関数の型付け

関数は、TypeScriptの重要な構成要素の1つです。関数の引数と返り値に型を指定することで、コードの可読性と保守性を高めることができます。この章では、関数の型付けについて説明します。

3.1. 関数の引数と返り値の型指定

関数の引数と返り値の型は、次のように指定します:

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

この例では、add関数は2つのnumber型の引数abを取り、number型の値を返します。

3.2. オプショナルパラメータとデフォルトパラメータ

関数の引数をオプショナルにするには、引数名の後に?を付けます。また、デフォルト値を指定することもできます。

function greet(name: string, greeting?: string): string {
  if (greeting) {
    return `${greeting}, ${name}!`;
  } else {
    return `Hello, ${name}!`;
  }
}

console.log(greet("John")); // "Hello, John!"
console.log(greet("John", "Hi")); // "Hi, John!"

この例では、greeting引数はオプショナルです。greetingが指定されない場合は、デフォルトの挨拶文が使用されます。

デフォルトパラメータを使用すると、次のようにも書けます:

function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

3.3. レストパラメータ

レストパラメータを使用すると、可変長引数を配列として受け取ることができます。レストパラメータは、引数リストの最後に配置し、...を使って宣言します。

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

console.log(sum(1, 2, 3, 4, 5)); // 15

この例では、sum関数は任意の数のnumber型引数を受け取り、それらの合計を返します。

3.4. オーバーロードされた関数

オーバーロードを使用すると、同じ名前の関数に複数の型シグネチャを定義できます。

function formatValue(value: string): string;
function formatValue(value: number): string;
function formatValue(value: string | number): string {
  if (typeof value === "string") {
    return `"${value}"`;
  } else {
    return `${value}`;
  }
}

console.log(formatValue("hello")); // ""hello""
console.log(formatValue(42)); // "42"

この例では、formatValue関数はstring型またはnumber型の引数を受け取ります。引数の型に応じて、適切な型シグネチャが選択されます。

3.5. コールバック関数の型付け

コールバック関数の型は、アロー関数の型シグネチャを使って指定できます。

function map<T, U>(array: T[], callback: (item: T) => U): U[] {
  return array.map(callback);
}

const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = map(numbers, (num) => num * 2);
console.log(doubledNumbers); // [2, 4, 6, 8, 10]

この例では、map関数はジェネリック型TUを使用しています。callback引数は、T型の引数を受け取り、U型の値を返す関数です。

これで、関数の型付けについての説明が終わりました。次の章では、オブジェクト指向プログラミングについて説明します。

4. オブジェクト指向プログラミング

TypeScriptは、クラスベースのオブジェクト指向プログラミングをサポートしています。この章では、TypeScriptでのクラスの定義方法、継承、アクセス修飾子などについて説明します。

4.1. クラスの定義

TypeScriptでは、classキーワードを使用してクラスを定義します。

class Person {
  name: string;
  age: number;

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

  sayHello() {
    console.log(
      `Hello, my name is ${this.name} and I'm ${this.age} years old.`,
    );
  }
}

この例では、Personクラスはnameageのプロパティを持ち、constructorでこれらのプロパティを初期化します。また、sayHelloメソッドを定義しています。

4.2. コンストラクタとプロパティ

コンストラクタは、クラスのインスタンスを作成するときに呼び出される特別なメソッドです。コンストラクタでは、クラスプロパティを初期化します。

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

この例では、constructorの引数にpublic修飾子を使用しています。これにより、nameageのプロパティが自動的に作成され、初期化されます。

4.3. メソッドの定義

クラス内でメソッドを定義するには、関数と同じ構文を使用します。

class Person {
  // ...

  sayHello() {
    console.log(
      `Hello, my name is ${this.name} and I'm ${this.age} years old.`,
    );
  }
}

4.4. 継承とサブクラス

TypeScriptでは、extendsキーワードを使用してクラスを継承できます。

class Employee extends Person {
  constructor(
    name: string,
    age: number,
    public department: string,
  ) {
    super(name, age);
  }

  sayHello() {
    super.sayHello();
    console.log(`I work in the ${this.department} department.`);
  }
}

この例では、EmployeeクラスはPersonクラスを継承しています。superキーワードを使用して、基底クラスのコンストラクタとメソッドにアクセスします。

4.5. アクセス修飾子(public, private, protected)

TypeScriptには、3つのアクセス修飾子があります:

  • public:どこからでもアクセス可能(デフォルト)
  • private:クラス内からのみアクセス可能
  • protected:クラス内とサブクラスからアクセス可能
class Person {
  private id: number;
  protected name: string;
  public age: number;

  // ...
}

4.6. 静的プロパティとメソッド

静的プロパティとメソッドは、クラスのインスタンスではなく、クラス自体に属します。静的メンバーには、staticキーワードを使用してアクセスします。

class MathUtils {
  static PI = 3.14159;

  static calculateCircumference(radius: number) {
    return 2 * MathUtils.PI * radius;
  }
}

console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.calculateCircumference(5)); // 31.4159

4.7. 抽象クラスと抽象メソッド

抽象クラスは、他のクラスが継承するための基本的な機能を提供します。抽象クラスは、abstractキーワードを使用して定義し、直接インスタンス化することはできません。

abstract class Shape {
  abstract calculateArea(): number;
}

class Rectangle extends Shape {
  constructor(
    private width: number,
    private height: number,
  ) {
    super();
  }

  calculateArea() {
    return this.width * this.height;
  }
}

この例では、Shapeは抽象クラスであり、calculateAreaは抽象メソッドです。RectangleクラスはShapeを継承し、calculateAreaメソッドを実装しています。

これで、TypeScriptでのオブジェクト指向プログラミングの基本について説明が終わりました。次の章では、インターフェースと型エイリアスについて詳しく説明します。

5. インターフェースと型エイリアス

インターフェースと型エイリアスは、TypeScriptで型を定義するための強力な機能です。この章では、インターフェースと型エイリアスの定義方法、違い、使用例について説明します。

5.1. インターフェースの定義と利用

インターフェースは、オブジェクトの型を定義するための契約のようなものです。interfaceキーワードを使用してインターフェースを定義します。

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

function greetPerson(person: Person) {
  console.log(`Hello, ${person.name}!`);
  person.sayHello();
}

const john: Person = {
  name: "John",
  age: 25,
  sayHello() {
    console.log(`I'm ${this.age} years old.`);
  },
};

greetPerson(john);

この例では、Personインターフェースを定義し、greetPerson関数の引数の型として使用しています。johnオブジェクトはPersonインターフェースを満たしているため、greetPerson関数に渡すことができます。

5.2. 型エイリアスの定義と利用

型エイリアスは、既存の型に新しい名前を付けるために使用します。typeキーワードを使用して型エイリアスを定義します。

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

function displayAge(age: Age) {
  console.log(`The age is ${age}.`);
}

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

displayAge(person.age);

この例では、Age型エイリアスをnumber型に、Person型エイリアスをオブジェクト型に割り当てています。

5.3. インターフェースと型エイリアスの違い

インターフェースと型エイリアスは似ていますが、いくつかの重要な違いがあります:

  • インターフェースは拡張可能ですが、型エイリアスは拡張できません。
  • インターフェースは宣言のマージが可能ですが、型エイリアスはできません。
  • インターフェースはオブジェクトの型を定義するために使用されますが、型エイリアスはプリミティブ型、ユニオン型、タプル型など、あらゆる型に使用できます。

5.4. 拡張インターフェースと交差型

インターフェースは、extendsキーワードを使用して拡張できます。型エイリアスでは、交差型(&)を使用して型を組み合わせることができます。

interface Animal {
  name: string;
}

interface Pet extends Animal {
  owner: string;
}

type Dog = Animal & {
  breed: string;
};

const pet: Pet = {
  name: "Max",
  owner: "John",
};

const dog: Dog = {
  name: "Buddy",
  breed: "Labrador",
};

この例では、PetインターフェースはAnimalインターフェースを拡張しています。Dog型エイリアスは、Animal型と{ breed: string }型の交差型です。

5.5. 辞書型とインデックスシグネチャ

辞書型は、キーと値のペアのコレクションを表すオブジェクト型です。TypeScriptでは、インデックスシグネチャを使用して辞書型を定義できます。

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

const ages: Dictionary<number> = {
  John: 25,
  Jane: 30,
  Bob: 35,
};

console.log(ages["Jane"]); // 30

この例では、Dictionary<T>インターフェースはジェネリック型Tを使用し、文字列キーとT型の値を持つオブジェクトを表します。

これで、インターフェースと型エイリアスについての説明が終わりました。次の章では、型定義ファイル(.d.ts)について説明します。

6. 型定義ファイル(.d.ts)

型定義ファイル(.d.ts)は、TypeScriptコンパイラに型情報を提供するファイルです。これらのファイルは、JavaScriptライブラリやモジュールの型を定義するために使用され、TypeScriptとの互換性を提供します。

6.1. 型定義ファイルの役割と利用方法

JavaScriptライブラリやモジュールには、元々TypeScriptの型情報が含まれていません。型定義ファイルは、これらのライブラリやモジュールの型情報を提供することで、TypeScriptプロジェクトで使用できるようにします。

型定義ファイルを利用するには、まず型定義ファイルをインストールする必要があります。多くの一般的なJavaScriptライブラリの型定義ファイルは、DefinitelyTypedリポジトリで提供されています。

6.2. @typesによる型定義のインストール

型定義ファイルをインストールするには、npm(Node.js Package Manager)を使用します。型定義ファイルのパッケージ名は、@types/プレフィックスで始まります。

例えば、jQueryの型定義ファイルをインストールするには、以下のコマンドを実行します:

npm install --save-dev @types/jquery

これにより、node_modules/@types/jqueryディレクトリに型定義ファイルがインストールされます。

6.3. 独自の型定義ファイルの作成

独自のJavaScriptライブラリやモジュールを作成する場合、または既存のライブラリに型定義ファイルが提供されていない場合は、自分で型定義ファイルを作成する必要があります。

型定義ファイルを作成するには、.d.ts拡張子のファイルを作成し、その中で型情報を定義します。

// my-library.d.ts

declare namespace MyLibrary {
  function doSomething(value: string): void;
  let version: string;
}

この例では、MyLibrary名前空間を定義し、doSomething関数とversion変数の型情報を提供しています。

6.4. 型定義ファイルのマージとオーバーライド

型定義ファイルは、マージとオーバーライドをサポートしています。複数の型定義ファイルで同じ名前空間やモジュールを定義すると、それらの定義がマージされます。

// library.d.ts

declare namespace Library {
  function doSomething(value: string): void;
}

// library-extensions.d.ts

declare namespace Library {
  function doSomethingElse(value: number): void;
}

この例では、library.d.tslibrary-extensions.d.tsの両方でLibrary名前空間を定義しています。これらの定義はマージされ、最終的なLibrary名前空間にはdoSomethingdoSomethingElseの両方の関数が含まれます。

また、型定義ファイルでモジュールやクラスを再定義することで、既存の型定義をオーバーライドすることもできます。

これで、型定義ファイル(.d.ts)についての説明が終わりました。次の章では、プロジェクトリファレンスについて説明します。

7. プロジェクトリファレンス

プロジェクトリファレンスは、TypeScriptプロジェクトを構造化し、プロジェクト間の依存関係を管理するための機能です。プロジェクトリファレンスを使用すると、大規模なTypeScriptプロジェクトを小さな独立したプロジェクトに分割し、ビルドパフォーマンスを向上させることができます。

7.1. プロジェクトリファレンスの概念

プロジェクトリファレンスは、TypeScriptの「composite」機能を使用して実装されます。各プロジェクトは、独自のtsconfig.jsonファイルを持ち、他のプロジェクトへの参照を定義します。

プロジェクトリファレンスを使用する主な利点は以下の通りです:

  • コードの構造化:大規模なプロジェクトを小さな独立したプロジェクトに分割することで、コードの管理がしやすくなります。
  • ビルドパフォーマンスの向上:変更されたプロジェクトのみを再コンパイルすることで、ビルド時間を短縮できます。
  • 増分ビルド:プロジェクト間の依存関係を管理することで、増分ビルドが可能になります。

7.2. tsconfig.jsonでのプロジェクトリファレンスの設定

プロジェクトリファレンスを設定するには、tsconfig.jsonファイルでreferencesオプションを使用します。

// tsconfig.json

{
  "compilerOptions": {
    "composite": true
    // その他のオプション
  },
  "references": [{ "path": "../shared" }, { "path": "../utils" }]
}

この例では、現在のプロジェクトが../shared../utilsプロジェクトに依存していることを示しています。compositeオプションをtrueに設定することで、プロジェクトリファレンスが有効になります。

7.3. 複数プロジェクト間の依存関係の管理

プロジェクトリファレンスを使用すると、複数のプロジェクト間の依存関係を管理できます。各プロジェクトは、他のプロジェクトで定義された型を使用できます。

例えば、sharedプロジェクトで定義された型をmainプロジェクトで使用するには、以下のように設定します:

// shared/tsconfig.json

{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "dist"
  }
}

// main/tsconfig.json

{
  "compilerOptions": {
    "composite": true,
    "outDir": "dist"
  },
  "references": [
    { "path": "../shared" }
  ]
}

この例では、sharedプロジェクトは型定義ファイル(.d.ts)を生成するためにdeclarationオプションをtrueに設定しています。mainプロジェクトは、referencesオプションでsharedプロジェクトへの参照を定義しています。

7.4. プロジェクトリファレンスを用いたビルドの最適化

プロジェクトリファレンスを使用すると、ビルドプロセスを最適化できます。TSCは、変更されたプロジェクトとその依存プロジェクトのみをリビルドし、ビルド時間を短縮します。

プロジェクトリファレンスを使用してビルドするには、--buildフラグを使用します:

tsc --build main/tsconfig.json

この例では、mainプロジェクトとその依存プロジェクトがビルドされます。

これで、プロジェクトリファレンスについての説明が終わりました。次の章では、TypeScriptの高度な型機能について説明します。

8. 高度な型機能

TypeScriptには、型システムをより柔軟かつ表現力豊かにする高度な機能がいくつかあります。この章では、ユニオン型とインターセクション型、リテラル型、型ガード、型アサーション、Nullable型とオプショナルチェイニングについて説明します。

8.1. ユニオン型とインターセクション型

ユニオン型(|)は、複数の型のいずれかを表します。インターセクション型(&)は、複数の型の全ての特性を持つ型を表します。

type Union = string | number;
type Intersection = { a: string } & { b: number };

const value1: Union = "hello";
const value2: Union = 42;

const obj: Intersection = { a: "hello", b: 42 };

この例では、Union型はstring型またはnumber型のいずれかを表します。Intersection型は、{ a: string }型と{ b: number }型の両方の特性を持つオブジェクト型を表します。

8.2. リテラル型

リテラル型は、特定のリテラル値のみを許容する型です。文字列、数値、真偽値のリテラル型を定義できます。

type Direction = "North" | "East" | "South" | "West";

function move(direction: Direction) {
  console.log(`Moving ${direction}.`);
}

move("North"); // OK
move("Northeast"); // Error

この例では、Direction型は"North""East""South""West"のいずれかの文字列リテラルを表します。

8.3. 型ガード

型ガードは、条件文を使用して変数の型を絞り込むためのテクニックです。typeofinstanceofin演算子、またはカスタムの型ガード関数を使用できます。

// typeof演算子を使用した型ガード
function printLength(value: string | string[]) {
  if (typeof value === "string") {
    console.log(value.length);
  } else {
    console.log(value.join(", ").length);
  }
}

printLength("Hello"); // 出力: 5
printLength(["Hello", "World"]); // 出力: 11

// instanceof演算子を使用した型ガード
class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  breed: string;
  constructor(name: string, breed: string) {
    super(name);
    this.breed = breed;
  }
  bark() {
    console.log("Woof!");
  }
}

class Cat extends Animal {
  constructor(name: string) {
    super(name);
  }
  meow() {
    console.log("Meow!");
  }
}

function printAnimal(animal: Animal) {
  console.log(animal.name);
  if (animal instanceof Dog) {
    console.log(animal.breed);
    animal.bark();
  } else if (animal instanceof Cat) {
    animal.meow();
  }
}

const dog = new Dog("Buddy", "Labrador");
const cat = new Cat("Whiskers");

printAnimal(dog); // 出力: "Buddy", "Labrador", "Woof!"
printAnimal(cat); // 出力: "Whiskers", "Meow!"

// in演算子を使用した型ガード
interface A {
  a: number;
}

interface B {
  b: string;
}

function printValue(value: A | B) {
  if ("a" in value) {
    console.log(value.a);
  } else {
    console.log(value.b);
  }
}

const valueA: A = { a: 42 };
const valueB: B = { b: "Hello" };

printValue(valueA); // 出力: 42
printValue(valueB); // 出力: "Hello"

// カスタムの型ガード関数
function isString(value: any): value is string {
  return typeof value === "string";
}

function isNumber(value: any): value is number {
  return typeof value === "number";
}

function printValue2(value: string | number) {
  if (isString(value)) {
    console.log(value.toUpperCase());
  } else if (isNumber(value)) {
    console.log(value.toFixed(2));
  }
}

printValue2("Hello"); // 出力: "HELLO"
printValue2(3.14159); // 出力: "3.14"

この例では、typeof型ガードを使用して、valuestring型またはstring[]型のどちらであるかを判断しています。

8.4. 型アサーション

型アサーションは、コンパイラに型の情報を伝える方法です。asキーワードまたは山括弧(<>)を使用して型アサーションを行います。

const value: unknown = "hello";
const length: number = (value as string).length;
// または
const length: number = (<string>value).length;

この例では、unknown型のvalue変数をstring型にアサーションしています。

8.5. Nullable型とオプショナルチェイニング

TypeScriptには、nullundefinedを明示的に扱うためのNullable型(?)とオプショナルチェイニング(?.)があります。

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

function printCity(user: User) {
  console.log(user.address?.city);
}

printCity({ name: "John" }); // undefined
printCity({
  name: "John",
  address: { street: "123 Main St", city: "New York" },
}); // "New York"

この例では、User型のageaddressプロパティはオプショナル(?)です。printCity関数内では、オプショナルチェイニング(?.)を使用して、user.addressが存在する場合にのみcityプロパティにアクセスしています。

これで、TypeScriptの高度な型機能についての説明が終わりました。次の章では、ジェネリックについて説明します。

9. ジェネリック

ジェネリックは、型を抽象化し、再利用可能で柔軟性の高いコードを書くためのTypeScriptの機能です。ジェネリックを使用すると、型を引数として受け取り、その型に基づいてコードを生成できます。

9.1. ジェネリック関数

ジェネリック関数は、型パラメータを受け取る関数です。型パラメータは、関数の引数や返り値の型を定義するために使用されます。

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

const result1 = identity<string>("hello");
const result2 = identity<number>(42);

この例では、identity関数は型パラメータTを受け取ります。引数argの型と返り値の型は、ともにT型です。

9.2. ジェネリッククラス

ジェネリッククラスは、型パラメータを受け取るクラスです。型パラメータは、クラスのプロパティやメソッドの型を定義するために使用されます。

class GenericStack<T> {
  private items: T[] = [];

  push(item: T) {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }
}

const stringStack = new GenericStack<string>();
stringStack.push("hello");
stringStack.push("world");

const numberStack = new GenericStack<number>();
numberStack.push(10);
numberStack.push(20);

この例では、GenericStackクラスは型パラメータTを受け取ります。itemsプロパティはT型の配列であり、pushメソッドとpopメソッドはT型の値を扱います。

9.3. ジェネリックインターフェース

ジェネリックインターフェースは、型パラメータを受け取るインターフェースです。型パラメータは、インターフェースのプロパティやメソッドの型を定義するために使用されます。

interface GenericValueHolder<T> {
  value: T;
}

const stringHolder: GenericValueHolder<string> = { value: "hello" };
const numberHolder: GenericValueHolder<number> = { value: 42 };

この例では、GenericValueHolderインターフェースは型パラメータTを受け取ります。valueプロパティはT型です。

9.4. 制約付きジェネリック

制約付きジェネリックは、型パラメータに制約を追加し、特定の型またはインターフェースを満たす型のみを許容するジェネリックです。extendsキーワードを使用して制約を定義します。

interface HasLength {
  length: number;
}

function printLength<T extends HasLength>(arg: T): void {
  console.log(arg.length);
}

printLength("hello"); // OK
printLength([1, 2, 3]); // OK
printLength(42); // Error

この例では、printLength関数はHasLengthインターフェースを満たす型のみを受け入れます。HasLengthインターフェースはlengthプロパティを持つ型に制約を設定しています。

9.5. デフォルトの型引数

ジェネリックにはデフォルトの型引数を指定できます。これにより、型引数が明示的に指定されない場合に使用されるデフォルトの型を定義できます。

class DefaultValueHolder<T = string> {
  constructor(public value: T) {}
}

const stringHolder = new DefaultValueHolder("hello");
const numberHolder = new DefaultValueHolder<number>(42);

この例では、DefaultValueHolderクラスは型パラメータTにデフォルト値としてstring型を指定しています。stringHolderインスタンスでは型引数を明示的に指定していないため、デフォルトのstring型が使用されます。numberHolderインスタンスでは型引数にnumber型を明示的に指定しています。

これで、ジェネリックについての説明が終わりました。次の章では、モジュールとネームスペースについて説明します。

10. モジュールとネームスペース

モジュールとネームスペースは、TypeScriptコードを構造化し、関連する機能をグループ化するための機能です。モジュールは、コードの再利用性と保守性を向上させ、名前の衝突を防ぐことができます。

10.1. モジュールの作成とエクスポート

TypeScriptでは、exportキーワードを使用してモジュールからコードをエクスポートできます。エクスポートされた要素は、他のモジュールからインポートして使用できます。

// math.ts

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

export const PI = 3.14159;

この例では、add関数とPI定数がエクスポートされています。

10.2. モジュールのインポート

エクスポートされた要素は、importキーワードを使用して他のモジュールからインポートできます。

// main.ts

import { add, PI } from "./math";

console.log(add(2, 3)); // 5
console.log(PI); // 3.14159

この例では、math.tsモジュールからadd関数とPI定数がインポートされています。

10.3. 外部モジュールと内部モジュール

TypeScriptには、外部モジュールと内部モジュールの2種類のモジュールシステムがあります。

外部モジュールは、それぞれ独自のファイルに定義されます。外部モジュールは、importexportを使用してコードを共有します。これは、現在のTypeScriptにおける標準的なモジュールシステムです。

内部モジュールは、1つのファイル内で定義されるモジュールです。内部モジュールは、namespaceキーワードを使用して定義します。内部モジュールは、レガシーなコードベースでよく使用されます。

// legacy.ts

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

console.log(MathUtils.add(2, 3)); // 5

この例では、MathUtils内部モジュールが定義され、add関数がエクスポートされています。

10.4. ネームスペースの使用

ネームスペースは、関連する機能をグループ化し、名前の衝突を防ぐために使用されます。ネームスペースは、namespaceキーワードを使用して定義します。

namespace Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }

  const lettersRegexp = /^[A-Za-z]+$/;
  const numberRegexp = /^[0-9]+$/;

  export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string): boolean {
      return lettersRegexp.test(s);
    }
  }

  export class NumbersOnlyValidator implements StringValidator {
    isAcceptable(s: string): boolean {
      return numberRegexp.test(s);
    }
  }
}

const strings = ["Hello", "98052", "101"];

const validators: { [s: string]: Validation.StringValidator } = {};
validators["Letters only"] = new Validation.LettersOnlyValidator();
validators["Numbers only"] = new Validation.NumbersOnlyValidator();

for (let s of strings) {
  for (let name in validators) {
    console.log(
      `"${s}" - ${validators[name].isAcceptable(s) ? "matches" : "does not match"} ${name}`,
    );
  }
}

この例では、Validationネームスペースが定義され、StringValidatorインターフェースとLettersOnlyValidatorおよびNumbersOnlyValidatorクラスが含まれています。ネームスペースを使用することで、関連する機能をグループ化し、名前の衝突を防いでいます。

これで、モジュールとネームスペースについての説明が終わりました。次の章では、非同期プログラミングについて説明します。

11. 非同期プログラミング

非同期プログラミングは、時間がかかる可能性のある操作を扱うための重要な概念です。TypeScriptは、コールバック関数、Promise、async/await、ジェネレータ関数など、さまざまな非同期プログラミングの手法をサポートしています。

11.1. コールバック関数

コールバック関数は、非同期操作が完了した後に呼び出される関数です。コールバック関数は、通常、非同期関数の引数として渡されます。

function fetchData(callback: (data: string) => void) {
  setTimeout(() => {
    const data = "Hello, world!";
    callback(data);
  }, 1000);
}

fetchData((data) => {
  console.log(data);
});

この例では、fetchData関数は非同期操作をシミュレートするためにsetTimeoutを使用しています。fetchData関数にコールバック関数を渡し、非同期操作が完了すると、そのコールバック関数が呼び出されます。

11.2. Promise

Promiseは、非同期操作の結果を表すオブジェクトです。Promiseは、非同期操作の状態(保留中、成功、失敗)を追跡し、結果値またはエラーを保持します。

function fetchData(): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = "Hello, world!";
      resolve(data);
    }, 1000);
  });
}

fetchData()
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.error(error);
  });

この例では、fetchData関数はPromiseを返します。Promiseが成功すると、thenメソッドが呼び出され、結果値が渡されます。Promiseが失敗すると、catchメソッドが呼び出され、エラーが渡されます。

11.3. async/await

async/awaitは、Promiseベースの非同期コードを同期的に見えるように書くための構文です。asyncキーワードを関数に付けると、その関数は必ずPromiseを返すようになります。awaitキーワードは、Promiseが解決されるまで関数の実行を一時停止します。

async function fetchData(): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const data = "Hello, world!";
      resolve(data);
    }, 1000);
  });
}

async function main() {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

main();

この例では、fetchData関数はasyncキーワードを使用して定義されています。main関数内では、awaitキーワードを使用してfetchData関数の結果を待っています。try/catchブロックを使用して、エラーを適切に処理しています。

11.4. ジェネレータ関数

ジェネレータ関数は、function*構文を使用して定義される特殊な関数です。ジェネレータ関数は、yieldキーワードを使用して値を生成し、一時停止と再開を可能にします。ジェネレータ関数は、非同期プログラミングに使用できます。

function* generatorFunction() {
  yield 1;
  yield 2;
  yield 3;
}

const generator = generatorFunction();
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log(generator.next().value); // 3

この例では、generatorFunctionはジェネレータ関数です。yieldキーワードを使用して値を生成しています。generator.next()を呼び出すたびに、ジェネレータ関数は次のyieldまで実行され、値を返します。

これで、TypeScriptにおける非同期プログラミングの基本的な概念の説明が終わりました。次の章では、デコレータについて説明します。

12. 高度なタイプシステム

TypeScriptは、高度で柔軟なタイプシステムを提供しています。この章では、条件付き型、マップ型、テンプレートリテラル型、キーワードインターフェース、レコード型など、TypeScriptの高度なタイプシステムの機能について説明します。

12.1. 条件付き型

条件付き型は、型の関係に基づいて型を選択する方法を提供します。条件付き型は、T extends U ? X : Yの形式で定義されます。ここで、TUに割り当て可能な場合はX型が使用され、そうでない場合はY型が使用されます。

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

type A = NonNullable<string | number | undefined>; // string | number
type B = NonNullable<string | string[] | null>; // string | string[]

この例では、NonNullable<T>条件付き型は、Tからnullundefinedを除外します。A型とB型は、NonNullable<T>を使用してnullundefinedを除外した型になります。

12.2. マップ型

マップ型を使用すると、既存の型からプロパティを変換して新しい型を作成できます。マップ型は、{ [P in keyof T]: T[P] }の形式で定義されます。ここで、Tは変換するソース型で、PTのプロパティキーを表します。

type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Partial<T> = { [P in keyof T]?: T[P] };

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

type ReadonlyPerson = Readonly<Person>;
type PartialPerson = Partial<Person>;

この例では、Readonly<T>マップ型は、Tのすべてのプロパティを読み取り専用にします。Partial<T>マップ型は、Tのすべてのプロパティをオプショナルにします。ReadonlyPerson型はPersonインターフェースを読み取り専用にし、PartialPerson型はPersonインターフェースのすべてのプロパティをオプショナルにします。

12.3. テンプレートリテラル型

テンプレートリテラル型を使用すると、文字列リテラルを型として使用できます。テンプレートリテラル型は、テンプレートリテラル構文を使用して定義されます。

type World = "world";
type Greeting = `Hello, ${World}!`;

type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";

type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;

この例では、Greeting型は、World型とテンプレートリテラル構文を使用して定義されています。AllLocaleIDs型は、EmailLocaleIDs型とFooterLocaleIDs型をユニオン型として組み合わせ、_idサフィックスを追加しています。

12.4. キーワードインターフェース(Partial, Required, Readonly, Pick, Omit)

TypeScriptには、型を変換するためのビルトインのキーワードインターフェースがいくつか用意されています。

  • Partial<T>Tのすべてのプロパティをオプショナルにします。
  • Required<T>Tのすべてのプロパティを必須にします。
  • Readonly<T>Tのすべてのプロパティを読み取り専用にします。
  • Pick<T, K>TからKで指定されたプロパティのみを選択します。
  • Omit<T, K>TからKで指定されたプロパティを除外します。
interface Person {
  name: string;
  age?: number;
  email: string;
}

type PartialPerson = Partial<Person>;
type RequiredPerson = Required<Person>;
type ReadonlyPerson = Readonly<Person>;
type PickedPerson = Pick<Person, "name" | "email">;
type OmittedPerson = Omit<Person, "age">;

この例では、PartialPersonRequiredPersonReadonlyPersonPickedPersonOmittedPersonの各型は、キーワードインターフェースを使用してPersonインターフェースから変換されています。

12.5. レコード型

レコード型は、キーの型と値の型を指定してオブジェクト型を定義するために使用します。レコード型は、Record<K, T>の形式で定義されます。ここで、Kはキーの型、Tは値の型を表します。

interface CatInfo {
  age: number;
  breed: string;
}

type CatName = "miffy" | "boris" | "mordred";

const cats: Record<CatName, CatInfo> = {
  miffy: { age: 10, breed: "Persian" },
  boris: { age: 5, breed: "Maine Coon" },
  mordred: { age: 16, breed: "British Shorthair" },
};

この例では、Record<CatName, CatInfo>を使用して、CatNameをキーとし、CatInfoを値とするオブジェクト型を定義しています。catsオブジェクトは、この型に適合しています。

これで、TypeScriptの高度なタイプシステムについての説明が終わりました。次の章では、TypeScriptのベストプラクティスについて説明します。

13. TypeScriptのベストプラクティス

TypeScriptを使用する際には、コードの品質、可読性、保守性を向上させるためのベストプラクティスに従うことが重要です。この章では、TypeScriptプロジェクトでのベストプラクティスについて説明します。

13.1. 型推論の活用

TypeScriptのタイプ推論機能を活用することで、冗長な型アノテーションを減らし、コードをすっきりと保つことができます。明示的な型指定が必要ない場合は、型推論に頼るようにしましょう。

// 型推論を活用しない例
const name: string = "John";
const age: number = 25;

// 型推論を活用する例
const name = "John";
const age = 25;

13.2. any型の使用を避ける

any型は、型チェックを回避するために使用されることがありますが、これはTypeScriptの型システムの利点を損なうことになります。any型の使用は最小限に抑え、できるだけ具体的な型を使用するようにしましょう。

// any型の使用例(避けるべき)
const data: any = { name: "John", age: 25 };

// 具体的な型の使用例
interface Person {
  name: string;
  age: number;
}
const data: Person = { name: "John", age: 25 };

13.3. 型安全性とパフォーマンスのバランス

型安全性とパフォーマンスのバランスを取ることが重要です。型チェックを厳しくしすぎると、開発速度が低下する可能性があります。一方で、型チェックが不十分だと、ランタイムエラーが発生するリスクが高くなります。プロジェクトの要件に応じて、適切なバランスを見つける必要があります。

// 型安全性が高すぎる例
function stringToNumber(str: string): number | null {
  const num = Number(str);
  return isNaN(num) ? null : num;
}

// バランスの取れた例
function stringToNumber(str: string): number {
  const num = Number(str);
  if (isNaN(num)) {
    throw new Error("Invalid number");
  }
  return num;
}

13.4. コードの再利用性を高める設計

コードの再利用性を高めることで、保守性と拡張性が向上します。共通の機能を抽象化し、ジェネリック型やインターフェースを活用することで、コードの再利用性を高めることができます。

// ジェネリック型を使用した再利用可能なコード
interface Repository<T> {
  getAll(): Promise<T[]>;
  getById(id: number): Promise<T | undefined>;
  create(entity: T): Promise<T>;
  update(entity: T): Promise<T>;
  delete(id: number): Promise<void>;
}

class UserRepository implements Repository<User> {
  // ... Repository<User>の実装 ...
}

class ProductRepository implements Repository<Product> {
  // ... Repository<Product>の実装 ...
}

この例では、Repository<T>ジェネリックインターフェースを定義し、UserRepositoryProductRepositoryクラスでそれぞれ具体的な型を指定しています。これにより、共通のリポジトリ機能を再利用可能な形で定義できます。

これらのベストプラクティスに従うことで、TypeScriptプロジェクトのコードの品質、可読性、保守性が向上し、開発効率が高まります。

これで、TypeScriptの中級者レベルに到達するための主要なトピックについての説明が終わりました。次の章では、TypeScriptのプロジェクト設定について説明します。

14. TypeScriptのプロジェクト設定

TypeScriptプロジェクトを適切に設定することは、コードの一貫性と品質を維持するために重要です。この章では、tsconfig.jsonファイルの設定項目、コンパイルオプションの最適化、プロジェクトの構成とディレクトリ構造について説明します。

14.1. tsconfig.jsonの設定項目

tsconfig.jsonファイルは、TypeScriptプロジェクトのコンパイルオプションと設定を指定するために使用されます。以下は、よく使用される設定項目の一部です:

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.spec.ts"]
}
  • compilerOptions:コンパイラの設定を指定します。
    • target:生成されるJavaScriptのバージョンを指定します。
    • module:生成されるモジュールコードのフォーマットを指定します。
    • strict:厳格な型チェックを有効にします。
    • esModuleInterop:CommonJSモジュールとESモジュールの相互運用性を有効にします。
    • outDir:コンパイル後のファイルの出力ディレクトリを指定します。
    • rootDir:ソースファイルのルートディレクトリを指定します。
  • include:コンパイルに含めるファイルやディレクトリを指定します。
  • exclude:コンパイルから除外するファイルやディレクトリを指定します。

14.2. コンパイルオプションの最適化

プロジェクトの要件に応じて、コンパイルオプションを最適化することが重要です。以下は、パフォーマンスと型安全性のバランスを取るための一般的な設定です:

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

これらのオプションは、厳格な型チェックと潜在的なエラーの検出を有効にします。ただし、プロジェクトによっては、一部のオプションを無効にしてパフォーマンスを優先させる必要がある場合もあります。

14.3. プロジェクトの構成とディレクトリ構造

プロジェクトの構成とディレクトリ構造を適切に設計することで、コードの可読性と保守性が向上します。以下は、一般的なTypeScriptプロジェクトのディレクトリ構造の例です:

my-project/
├── src/
│   ├── controllers/
│   ├── models/
│   ├── services/
│   ├── utils/
│   └── index.ts
├── tests/
│   ├── unit/
│   └── integration/
├── dist/
├── node_modules/
├── package.json
└── tsconfig.json
  • src:TypeScriptのソースコードを格納します。
    • controllers:アプリケーションのコントローラーを格納します。
    • models:アプリケーションのモデルを格納します。
    • services:アプリケーションのサービスを格納します。
    • utils:ユーティリティ関数を格納します。
    • index.ts:アプリケーションのエントリーポイントです。
  • tests:テストコードを格納します。
    • unit:ユニットテストを格納します。
    • integration:統合テストを格納します。
  • dist:コンパイル後のJavaScriptファイルを格納します。
  • node_modules:プロジェクトの依存関係を格納します。
  • package.json:プロジェクトの情報と依存関係を定義します。
  • tsconfig.json:TypeScriptのコンパイルオプションと設定を指定します。

これは一例ですが、プロジェクトの規模や要件に応じて、ディレクトリ構造をカスタマイズすることができます。

適切なプロジェクト設定とディレクトリ構造を維持することで、コードの一貫性と品質が向上し、開発とメンテナンスがしやすくなります。

これで、TypeScript初心者から中級者レベルに到達するための完全ガイドが終了しました。今後は、このガイドで学んだ知識を活用して、実際のプロジェクトで TypeScript を使用していくことをお勧めします。TypeScript の公式ドキュメントや コミュニティリソースを参考にしながら、さらにスキルを磨いていってください。

logo

Web Developer。パフォーマンス改善、データ分析基盤、生成AIに興味があり。Next.js, Terraform, AWS, Rails, Pythonを中心に開発スキルを磨いています。技術に関して幅広く投稿していきます。