変化の激しいIT業界において、JavaScriptは動的なウェブアプリケーションを構築するための主要な言語となっています。しかし、JavaScriptの動的な型付けは、時に厄介なエラーを引き起こす可能性があり、さらに開発プロセスの初期段階でエラーを発見することは困難です。

そこで、JavaScriptのコードの書き方に革命をもたらすTypeScriptの登場です。

この記事では、TypeScriptについて深く掘り下げ、その特徴、メリット、ベストプラクティスに迫ります。また、TypeScriptがJavaScriptの制限にいかに対処できるのか、そして、堅牢でスケーラブルなウェブアプリケーションの構築において静的型付けがどう役立つのかもご紹介します。

それでは、本題に進みましょう。

TypeScriptとは

TypeScriptは、JavaScriptのスーパーセットで、JavaScriptにオプションの静的型付けと高度な機能を追加するものです。Microsoftによって開発されました。2012年にリリースされ、ウェブ開発コミュニティで広く採用されるようになりました。

2022年に行われたStack Overflowの開発者調査によると、TypeScriptは73.46%で4番目に使われている技術とのこと。TypeScriptは、JavaScriptの制限事項である強い型付けの欠如に対処するために作られました。

例えば、次のようなJavaScriptのコードを考えてみましょう。

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

let result = add(10, "20"); // No error, but result is "1020" instead of 30

上のコードでは、動的型付けされた関数addを作成しています。引数abの型は強制されません。その結果、引数として数値の代わりに文字列を渡してもエラーにならず、代わりに文字列として値が連結され、予期せぬ動作につながります。

TypeScriptでは、オプションの静的型付けが導入され、開発者は変数、関数のパラメータ、戻り値の型を指定することができ、これにより開発中に型に関するエラーを検出することができます。

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

let result = add(10, "20"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'

上のTypeScriptのコードでは、パラメータabの型が数値として明示的に定義されています。引数として文字列が渡された場合、TypeScriptはコンパイル時にエラーを発生させ、潜在的な問題を早期に発見できるようにします。

TypeScriptの特徴

TypeScriptは、JavaScriptの制限のいくつかに対処する、最新のウェブ開発に欠かせない強力な機能を誇ります。これを使うことにより、開発者の体験とコードの構成が強化されます。詳しくは以下の通りです。

1. 静的な型付け

TypeScriptは強力な型付けシステムを備えており、コンパイル時に変数や関数のパラメータの型を指定することができます。これにより、型に関連するエラーを早期に検出することができ、コードの信頼性が高まり、バグの発生が少なくなります。

一方、JavaScriptでは、変数は動的に型付けされ、実行時に型が変わる可能性があります。

例えば、以下のJavaScriptのコードでは、動的に型付けされた2つの変数(数値と文字列)を宣言しています。

let num1 = 10; // num1を動的に型付け
let num2 = "20"; // num2を動的に型付け

let result = num1 + num2; // コンパイル時にエラーが発生しない
console.log(result); // 出力されるもの:"1020"

このコードでは、数値と文字列の連結である「1020」が出力されます。これは、本来期待される結果ではありません。つまり、これがコードに影響を与える可能性があるということです。JavaScriptの欠点は、ここでエラーを投げることができないことです。TypeScriptでは、各変数の型を指定することで、これを解決することができます。

let num1: number = 10; // num1を静的に数値として型付けしている
let num2: string = "20"; // num2を静的に文字列として型付けしている

let result = num1 + num2; // エラー:Type 'string' is not assignable to type 'number'

上のコードでは、+演算子を使用して数値と文字列を連結しようとすると、コンパイル時にエラーが発生します。これは、TypeScriptが厳格な型のチェックを実施しているためです。

TypeScriptのチェックにより、コードを実行する前に型に関連する潜在的なバグを検出し、より堅牢でエラーのないコードにすることができます。

2. 型付けの選択

TypeScriptでは、静的型付けを使用するかどうかを柔軟に選択することができます。つまり、変数や関数のパラメータに型を指定するか、割り当てられた値に基づいてTypeScriptが自動的に型を推測するかを選択することができます。

たとえば、以下のようになります。

let num1: number = 10; // 数値として静的に型付け
let num2 = "20"; // 文字列として記述

let result = num1 + num2; // エラー:Operator '+' cannot be applied to types 'number' and 'string'

このコードでは、num2の型は、割り当てられた値に基づいてstringと推測されていますが、必要に応じて型を指定することも可能です。

また、型をanyに設定することもできます。これは、どのような種類の値でも受け入れることを意味します。

let num1: number = 10;
let num2: any = "20";

let result = num1 + num2; // エラー:Operator '+' cannot be applied to types 'number' and 'string'

3. ES6+の機能

TypeScriptは、ECMAScript 6(ES6)以降のバージョンで導入されたものを含む、最新のJavaScriptの機能をサポートしています。

これにより、アロー関数、分割代入、テンプレートリテラルなどの機能を使って、よりクリーンで表現力豊かなコードを書きながら、型のチェックを行うことができます。

例えば以下のようになります。

const greeting = (name: string): string => {
  return `Hello, ${name}!`; // アロー関数とテンプレートリテラルの使用
};

console.log(greeting("John")); // 出力:Hello, John!

このコードでは、アロー関数とテンプレートリテラルが使われています。これはすべてのJavaScriptの構文に共通します。

4. コードの整理

JavaScriptでは、コードベースが大きくなるにつれて、コードを別々のファイルに整理し、依存関係を管理することが困難になりがちです。TypeScriptには、コードを整理するためのモジュールと名前空間のサポートが組み込まれています。

モジュールでは、コードを別々のファイル内にカプセル化することができ、大規模なコードベースの管理と維持が容易になります。

以下はその例です。

// greeting.ts:
export function greet(name: string): string { // モジュールから関数をエクスポート
  return `Hello, ${name}!`;
}

// app.ts:
import { greet } from "./greeting"; // モジュールからインポート

console.log(greet("John")); // 出力:Hello, John!

上記の例では、greeting.tsapp.tsの2つのファイルがあります。app.tsファイルは、importを使用してgreeting.tsファイルからgreet関数をインポートします。greeting.tsファイルは、exportを使用してgreet関数をエクスポートし、他のファイルでのインポートにアクセスできるようにしています。

これにより、より良いコード構成と懸念事項の分離が可能になり、大規模なコードベースの管理・維持が容易になります。

TypeScriptの名前空間により、関連するコードをグループ化し、グローバルな名前空間の汚染を回避できます。これは、関連するクラス、インターフェース、関数、変数の集合のためのコンテナを定義するのに使用可能です。

以下はその例です。

namespace Utilities {
  export function greet(name: string): string {
    return `Hello, ${name}!`;
  }
  
  export function capitalize(str: string): string {
    return str.toUpperCase();
  }
}

console.log(Utilities.greet("John")); // 出力:Hello, John!
console.log(Utilities.capitalize("hello")); // 出力:HELLO

このコードでは、namespace Utilitiesを定義し、greetcapitalizeという2つの関数を含んでいます。これらの関数には、名前空間名の後に関数名を付けてアクセスすることができ、関連するコードを論理的にグループ化可能です。

5. オブジェクト指向プログラミング(OOP)

TypeScriptは、クラス、インターフェース、継承などのOOPの概念をサポートし、構造化/整理されたコードを実現します。

たとえば、以下のようなものです。

class Person {
  constructor(public name: string) {} // コンストラクタでクラスを定義
  greet(): string { // クラスで内でメソッドを定義
    return `Hello, my name is ${this.name}!`;
  }
}

const john = new Person("John"); // クラスのインスタンスを作成
console.log(john.greet()); // 出力:Hello, my name is John!

6. 高度な型システム

TypeScriptには、ジェネリクス、ユニオン、インターセクションなどをサポートする高度な型システムが用意されています。これにより、TypeScriptの静的な型チェック機能が強化され、より堅牢で表現力豊かなコードを書くことができます。

ジェネリクス:ジェネリクスを使用すると、さまざまな型を扱う再利用可能なコードを書くことができます。ジェネリクスは、関数やクラスに渡される値に基づいて実行時に決定される、型のプレースホルダーのようなものです。

たとえば、T型の引数を受け取り、同じ型の値Tを返すジェネリック関数identityを定義してみましょう。

function identity(value: T): T {
  return value;
}

let num: number = identity(10); // Tが数値として推測される
let str: string = identity("hello"); // Tが文字列として推測される

上記の例では、関数に渡された値の型に基づいて型Tが推測されます。identity関数の最初の使い方では、引数として10を渡しているため、Tは数値と推論され、2番目の使い方では、引数として"hello"を渡しているため、Tは文字列と推論されています。

ユニオンとインターセクション:ユニオンとインターセクションは、型を合成し、より複雑な型の関係を作成するために使用されます。

ユニオンは、2つ以上の型を組み合わせて、組み合わせた型のいずれかを持ち得る1つの型にすることができます。インターセクションは、2つ以上の型を組み合わせて、組み合わせた型のすべてを満たす必要のある1つの型にすることができます。

例えば、社員とマネージャーを表すEmployeeManagerという2つの型を定義することができます。

type Employee = { name: string, role: string };
type Manager = { name: string, department: string };

EmployeeManagerを使って、EmployeeまたはManagerのどちらかになるユニオン型EmployeeOrManagerを定義することができます。

type EmployeeOrManager = Employee | Manager; // ユニオン型

let person1: EmployeeOrManager = { name: "John", role: "Developer" }; // EmployeeまたはManager

上のコードでは、person1変数はEmployeeOrManagerであり、EmployeeまたはManagerのいずれかを満たすオブジェクトを代入することができます。

また、EmployeeManagerの両方の型を満たす必要があるインターセクション型EmployeeOrManagerを定義することもできます。

type EmployeeAndManager = Employee & Manager; // インターセクション型

let person2: EmployeeAndManager = { name: "Jane", role: "Manager", department: "HR" }; // EmployeeとManagerの両方でなければならない

上記のコードでは、person2変数はEmployeeAndManagerです。これは、EmployeeManagerの両方を満たすオブジェクトでなければならないことを意味します。

7. JavaScriptとの互換性

TypeScriptはJavaScriptのスーパーセットとして設計されており、有効な JavaScriptのコードはすべて有効なTypeScriptのコードでもあることを意味します。このため、すべてのコードを書き直すことなく、既存のJavaScriptプロジェクトにTypeScriptを簡単に統合することができます。

TypeScriptはJavaScript上に構築され、オプションの静的型付けやその他の機能を追加していますが、素のJavaScriptのコードをそのまま使用することもできます。

たとえば、既存のJavaScriptファイルapp.jsがあれば、app.tsに名前を変更して、既存のJavaScriptコードを変更することなく、TypeScriptの機能を少しずつ使い始めることが可能です。TypeScriptは、そのJavaScriptコードを有効なTypeScriptとして理解し、コンパイルすることができます。

以下に、TypeScriptがJavaScriptとシームレスに統合する例を紹介します。

// app.js - 既存のJavaScriptコード
function greet(name) {
  return "Hello, " + name + "!";
}

console.log(greet("John")); // 出力:Hello, John!

上記のJavaScriptファイルをapp.tsに変更して、TypeScriptの機能を使い始めます。

// app.ts - JavaScriptのコードをTypeScriptとして使う
function greet(name: string): string {
  return "Hello, " + name + "!";
}

console.log(greet("John")); // 出力:Hello, John!

上記の例では、nameパラメータに型を追加し、string、TypeScriptではオプションであることを指定しています。残りのコードはJavaScriptと同じです。TypeScriptはJavaScriptのコードを理解し、追加された型アノテーションの型チェックを行うことができるため、既存のJavaScriptプロジェクトにTypeScriptを徐々に導入することが容易になります。

TypeScriptの利用を始める

TypeScriptは公式のコンパイラであり、npmを使ってプロジェクトにインストールすることができます。プロジェクトでTypeScript5.0の利用を開始するには、プロジェクトのディレクトリで以下のコマンドを実行します。

npm install -D typescript

これにより、コンパイラがnode_modulesディレクトリにインストールされ、npx tscコマンドで実行できるようになります。

JavaScriptのプロジェクトでは、まず以下のコマンドで初期化しpackage.jsonファイルを作成する必要があります。

npm init -y

次に、TypeScriptの依存関係をインストールし、拡張子.tsを使用してTypeScriptファイルを作成し、TypeScriptコードを記述することができます。

TypeScriptのコードを書いたら、TypeScriptコンパイラ(tsc)を使ってJavaScriptにコンパイルします。プロジェクトディレクトリで以下のコマンドを実行します。

npx tsc .ts

これにより、指定したファイル内のTypeScriptコードがJavaScriptにコンパイルされ、同じ名前の.jsファイルが生成されます。

そして、通常のJavaScriptコードを実行するのと同じように、プロジェクト内でコンパイルされたJavaScriptコードを実行することができます。Node.jsを使用してNode.js環境でJavaScriptコードを実行したり、コンパイルしたJavaScriptファイルをHTMLファイルに含めてブラウザで実行したりすることができます。

インターフェースを使う

TypeScriptのインターフェースは、オブジェクトのcontractやshapeを定義するのに使用されます。これにより、オブジェクトが準拠すべき構造や形状を指定することができます。

インターフェースは、オブジェクトがインターフェースと互換性があるとみなされるために持つべきプロパティやメソッドを定義します。オブジェクト、関数パラメータ、戻り値の型注釈を扱うもので、IDEでのより良い静的型チェックとコード補完の提案を可能にします。

以下は、TypeScriptにおけるインターフェースの例です。

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

この例では、Personというインターフェースを定義し、3つのプロパティ(firstNamestringlastNamestringagenumber)を指定しています。

この3つのプロパティを指定された型で持つオブジェクトは、Personインターフェースと互換性があるとみなされます。では、Personインターフェースに適合するオブジェクトを定義してみましょう。

let person1: Person = {
  firstName: "John",
  lastName: "Doe",
  age: 30
};

let person2: Person = {
  firstName: "Jane",
  lastName: "Doe",
  age: 25
};

この例では、Personインターフェースに適合する2つのオブジェクトperson1person2を作成しています。両オブジェクトとも、必要なプロパティfirstNamelastNameageを指定された型で持っているため、Personインターフェースと互換性があります。

インターフェースの拡張

インターフェースを拡張して、既存のインターフェースのプロパティを継承した新しいインターフェースを作成することもできます。

例えば以下の通りです。

interface Animal {
  name: string;
  sound: string;
}

interface Dog extends Animal {
  breed: string;
}

let dog: Dog = {
  name: "Buddy",
  sound: "Woof",
  breed: "Labrador"
};

この例では、プロパティnamesoundを持つインターフェースAnimalを定義し、さらにAnimalインターフェースを拡張し、新しいプロパティbreedを追加した新しいインターフェース「Dog」を定義しています。DogインターフェースはAnimalインターフェースからプロパティを継承するため、Dogインターフェースに適合するオブジェクトは、プロパティnamesoundも持たなければなりません。

オプショナルプロパティ

インターフェースは、オプションのプロパティも持つことができます。プロパティ名の後に?が付きます。

以下はその例です。

interface Car {
  make: string;
  model: string;
  year?: number;
}

let car1: Car = {
  make: "Toyota",
  model: "Camry"
};

let car2: Car = {
  make: "Honda",
  model: "Accord",
  year: 2020
};

この例では、プロパティmakemodelを持つインターフェースCarで、オプションのプロパティyearを定義しています。yearプロパティは必須ではないので、Carインターフェースに適合するオブジェクトは、これを持つことも持たないこともあり得ます。

高度な型チェック

TypeScriptでは、tsconfig.jsonに型チェックのための高度なオプションが用意されています。これにより、TypeScriptプロジェクトの型チェック機能を強化し、コンパイル時に潜在的なエラーを検出することで、より堅牢で信頼性の高いコードを実現できます。

1. strictNullChecks

trueに設定すると、TypeScriptは厳密なnullチェックを実施します。つまり、nullまたはundefinedのユニオン型を明示的に指定しない限り、変数はnullまたはundefinedの値を持つことができません。

例えば、以下のようになります。

{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

このオプションを有効にすると、TypeScriptはコンパイル時にnullundefinedの潜在的な値を検出し、nullundefined変数のプロパティやメソッドにアクセスすることで発生する実行時のエラーを防ぐことができます。

// 例1:オブジェクトが'null'である可能性を示すエラー
let obj1: { prop: string } = null;
console.log(obj1.prop);

// 例2:オブジェクトが'undefined'である可能性を示すエラー
let obj2: { prop: string } = undefined;
console.log(obj2.prop);

2. strictFunctionTypes

trueに設定すると、TypeScriptは関数引数のずれを含む関数型の厳密なチェックを有効にし、関数引数の型の互換性が厳しくチェックされます。

例えば、以下のようになります。

{
  "compilerOptions": {
    "strictFunctionTypes": true
  }
}

このオプションを有効にすると、TypeScriptはコンパイル時に潜在的な関数パラメータの型の不一致を検出し、関数に不正な引数を渡すことによって生じる実行時エラーを防ぐことにつながります。

// エラー:Argument of type 'number' is not assignable to parameter of type 'string'
function greet(name: string) {
  console.log(`Hello, ${name}!`);
}

greet(123);

3. noImplicitThis

trueに設定すると、TypeScriptは暗黙のany型を持つthisの使用を禁止します。これは、クラスメソッドでthisを使用する際に起こりうるエラーを検出するのに役立ちます。

例えば、以下の通りです。

{
  "compilerOptions": {
    "noImplicitThis": true
  }
}

このオプションを有効にすると、TypeScriptはクラスメソッドで適切な型注釈やバインディングを行わずにthisを使用した場合に生じる潜在的なエラーをキャッチします。

// エラー:The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'
class MyClass {
  private prop: string;

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

  printProp() {
    console.log(this.prop);
  }
}

let obj = new MyClass("Hello");
setTimeout(obj.printProp, 1000); // 'this'のコンテクストが失われた潜在的エラー

4. ターゲット

targetは、TypeScriptコードのECMAScriptターゲットバージョンを指定します。TypeScriptコンパイラが出力として生成すべきJavaScriptのバージョンを決定することができます。

例えば、以下のようになります。

{
  "compilerOptions": {
    "target": "ES2018"
  }
}

このオプションを「ES2018」に設定すると、TypeScriptはECMAScript 2018に準拠したJavaScriptコードを生成します。

これは例えば、最新のJavaScriptの機能や構文を活用したいものの、古いJavaScript環境との後方互換性も確保する必要がある場合に有効です。

5. モジュール

moduleは、TypeScriptコードで使用されるモジュールシステムを指定します。一般的なオプションとしては、「CommonJS」、「AMD」、「ES6」、「ES2015」などがあります。これにより、TypeScriptモジュールがどのようにJavaScriptモジュールにコンパイルされるかが決定されます。

例えば以下の通りです。

{
  "compilerOptions": {
    "module": "ES6"
  }
}

このオプションを「ES6」に設定すると、TypeScriptはECMAScript 6のモジュール構文を使用するJavaScriptコードを生成します。

これは、ECMAScript 6モジュールをサポートする最新のJavaScript環境、たとえばwebpackやRollupのようなモジュールバンドラーを使用するフロントエンドアプリケーションで作業している場合に便利です。

6. noUnusedLocalsとnoUnusedParameters

これを利用することで、未使用のローカル変数と関数パラメータをキャッチすることができます。

trueに設定すると、宣言されているもののコードで使用されていないローカル変数や関数パラメータに対してコンパイルエラーが発生します。

例えば、以下のようになります。

{
  "compilerOptions": {
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

複数ご紹介しましたが、TypeScriptのtsconfig.jsonファイルにある高度なタイプチェックオプションのほんの一例に過ぎません。詳しくは、公式ドキュメントをご覧ください。

TypeScriptのベストプラクティス

1. 変数、関数のパラメータ、戻り値の型に適切な注釈を付ける

TypeScriptの主なメリットの1つは、強力な型付けシステムであり、変数、関数のパラメータ、および戻り値の型を明示的に指定できることです。

これにより、コードの可読性が向上し、型に関連する潜在的なエラーを早期に発見し、IDEでの高い精度でのコード補完が可能になります。

以下はその例です。

// 変数型の適切な注釈
let age: number = 25;
let name: string = "John";
let isStudent: boolean = false;
let scores: number[] = [98, 76, 89];
let person: { name: string, age: number } = { name: "John", age: 25 };

// 関数のパラメータと戻り値の型の適切な注釈
function greet(name: string): string {
  return "Hello, " + name;
}

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

2. TypeScriptの高度な型機能を効果的に活用する

TypeScriptには、ジェネリックス、ユニオン、インターセクション、条件付き、マップなど、高度な型機能が豊富に用意されています。これらの機能は、柔軟で再利用可能なコードを書く上で有用です。

以下はその例です。

// ジェネリクスを使用して再利用可能な関数を作成
function identity(value: T): T {
  return value;
}

let num: number = identity(42); // 推測される型:数値
let str: string = identity("hello"); // 推測される型:文字列

// ユニオン型を使って複数の型を使えるようにする
function display(value: number | string): void {
  console.log(value);
}

display(42); // 有効
display("hello"); // 有効
display(true); // エラー

3. TypeScriptで保守がしやすくスケーラブルなコードを書く

TypeScriptは、インターフェース、クラス、モジュールなどの機能を提供することで、保守のしやすくスケーラブルなコードを書くことを推奨しています。

その一例を紹介します。

// 契約を定義するインターフェースの使用
interface Person {
  name: string;
  age: number;
}

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

let john: Person = { name: "John", age: 25 };
console.log(greet(john)); // "Hello, John"

// クラスの利用によるカプセル化と抽象化
class Animal {
  constructor(private name: string, private species: string) {}

  public makeSound(): void {
    console.log("Animal is making a sound");
  }
}

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

  public makeSound(): void {
    console.log("Dog is barking");
  }
}

let myDog: Dog = new Dog("Buddy", "Labrador");
myDog.makeSound(); // "Dog is barking"

4. TypeScriptのツールとIDEを活用する

TypeScriptは、自動補完、型推論、リファクタリング、エラーチェックなどの機能を備えた、優れたツールやIDEをサポートしています。

これらの機能を活用して生産性を向上させ、開発プロセスの早い段階で潜在的なエラーを発見するのが得策です。Visual Studio CodeなどのTypeScript対応のIDEを使用し、TypeScriptプラグインをインストールすることで、コードの編集がより一層捗ります。

VSコードのTypeScriptエクステンション
VSコードのTypeScriptエクステンション

まとめ

TypeScriptは、ウェブ開発プロジェクトを大幅に強化できる強力な機能を幅広く搭載しています。

静的型付け、高度な型システム、およびオブジェクト指向プログラミング機能により、保守性、拡張性、および堅牢性の高いコードを記述するための貴重なツールとなっています。また、TypeScriptのツールやIDEのサポートにより、シームレスな開発体験が実現します。

Kinstaのアプリケーションホスティングをご利用ください。すぐに利用を開始し、TypeScriptを使ったコーディングを始めることができます。