Lesson 2

環境構築

Lesson 2 Chapter 1
Node.jsのインストール

Lesson2では、Node.jsの環境構築について解説します。Node.js本体はもちろん、コードエディタ「VSCode」やパッケージ管理ツール「npm」との連携も学んでいきましょう。

まずはNode.jsのインストールから始めます。手順に沿えば難しいことはありません。掲載した画像と見比べながら読み進めてください。

インストーラーをダウンロードする

Node.jsをインストールする方法として、最も手軽で確実なのは公式のインストーラーを使用することです。

すでにNode.jsをインストールしている方も、本教材に取り組むにあたって最新の推奨版をインストールしましょう。

※以下、掲載している画像はWindows11のものです。Macを使用している場合も基本的なインストールの流れは変わりません。環境に応じて読み替えるようにしてください。

Node.js公式サイト

node-hp.png Node.js 公式サイト画面

表示されているバージョンが2つありますが、左側に表示される「LTS」バージョンを選択してください。LTSとは「Long Term Support」の略で、公式サイトが長期サポートを明言している安定版です。

一方、右側に表示されているのは、安定性を約束しない代わりに追加機能を盛り込んだ最新版です。

新機能を試したいなどの理由が無い限りは、基本的にLTS版を使用するようにしてください。

インストーラーを起動する

ここからはインストーラーを操作します。インストーラーの画面を続けて掲載していますが、注釈が無い限りは「Next」をクリックして問題ありません。

node-setup-top.png

node-setup-terms.png

規約画面です。同意する場合は「I accept the terms in the Licence Agreement」にチェックを入れ、 「Next」を押して次画面に進みます。

node-setup-folder.png

インストール先を選択する画面です。変更する場合は「Change」から変更してください。

node-setup-custom.png

インストールするソフトウェア一覧です。前述した「npm」もここでインストールされます。

node-setup-module.png

インストールを自動化するために、関連ツール(Chocolatey)を導入するかどうか聞かれています。今回は必要ないためチェックを外して進みましょう。

node-setup-ready-install.png

node-setup-install.png

node-setup-completed.png

最後に「Finish」をクリックすれば完了です。

インストールできたか確認する

正常にインストールできたか、コマンドラインを立ち上げて確認しましょう。

コマンドラインの起動方法

Windows:画面左下のタスクバーに「cmd」と入力し、「コマンドプロンプト」をクリックします。 Mac:メニューバーのSpotlightアイコンをクリックし、「ターミナル」と入力。「ターミナル」をクリックします。

search-cmd.png

コマンドラインが起動したら、以下の通り入力してください。

コマンドプロンプト
node --version

公式サイトで選択したバージョンが表示されれば、問題なくインストールされています。

コマンドプロンプト
v16.16.0

これでNode.jsのインストールは完了です。

Node.jsは頻繁にアップデートが行われるため、プロジェクトによって複数のバージョンを使い分ける機会が頻繁に生じます。「NVM」などのバージョン管理ツールも多く公開されているため、積極的に活用するといいでしょう。

次のChapterでは、インストールしたNode.jsを実際に起動してみます。

Lesson 2 Chapter 2
Node.jsを動かしてみる

Chapter1では、Node.jsのインストール方法を解説しました。それでは、インストールしたNode.jsを実際に起動してみましょう。

Node.jsを初回起動する

まずは任意の場所に作業用ディレクトリを作成します。その中に以下のコードを書いたJavaScriptファイルを作成してください。

hello.js
console.log("Hello World!");

※ここでは「practice」というディレクトリと、「hello.js」というJavaScriptファイルを作って進めます。

続いて、コマンドプロンプトで以下のコードを実行します。

コマンドプロンプト
node hello.js

「Hello World!」の文字が出力されたら成功です。

簡単にコードを説明しましょう。「hello.js」ファイルに書いたconsole.logは、引数として渡された文字列を出力するコードです。

ここではHello Worldが渡された結果、コマンドライン上に同様の文字列が出力されました。

試しに上記のコードをconsole.log("hogehoge")に変更すると、「hogehoge」の文字が出力されるはずです。

参考:ディレクトリ(フォルダ)の移動

コマンドプロンプトでnode hello.jsを実行する際、カレントディレクトリが作業用ディレクトリであることを確認するようにしましょう。

以下のコマンドでカレントディレクトリを上の階層に変更できます。

1つ上のフォルダに移動するコマンド
cd ..\

たとえば、カレントディレクトリがC:\User\hoge\fugaであるとき、 上のコマンドを実行するとC:\User\hogeに移動します。

一方、次のコマンドを入力するとカレントディレクトリを指定した階層に変更できます。

1つ下のフォルダに移動するコマンド
cd (移動したいフォルダ名)

たとえば、カレントディレクトリがC:\User\hogeのとき、 cd fugaを実行すると、カレントディレクトリが C:\User\hoge\fugaに移動します。

ここで取り上げたコマンドライン操作は、Node.jsの基本です。今後も繰り返し使うことになるため、忘れずに覚えておいてください。

Lesson 2 Chapter 3
VSCodeのインストール

このChapterでは、ソースコードを書く際に便利なテキストエディタ「Visual Studio Code」(以下VSCode)のインストール手順を紹介します。

VSCodeは、Microsoft社が2015年に発表したテキストエディタ(コードエディタ)です。オープンソースとして無償提供されており、軽量な動作と拡張性の高さを特徴としています。

Node.js用の機能も充実しており、きっと皆さんの開発を後押ししてくれるはずです。今まで通常のテキストエディタを使用していた方も、この機会にVSCodeを体験してみましょう。

VSCodeをインストールする

今回もWindowsを想定してインストールを行います(MacやLinuxの場合は適宜読み替えてください)。

VSCodeのダウンロードページから、「Windows User installer」を選択します。

Visual Studio Codeのダウンロードページ

vscode-hp.png VSCodeダウンロード画面

Windowsの場合「x64」「x86」「Arm64」版が用意されています。自分のOSのビット数を調べ、64ビットであれば「x64」を、32ビットであれば「x86」を選択してください。

なお、Windowsのビット数はPCの「設定>システム>バージョン仕様」で確認できます。

system-type-bit.png 上記の場合は64bitの方をインストールします。

Arm64版

「Arm64」は、ARM版Windows 10/11を搭載している一部のモバイルPCが対象です。一般的な64ビット版である「x64」と間違えないように注意してください。

インストール方法による違い

ちなみに、Windowsの場合は同じバージョンでも3種類のインストール手段が提供されています。

  • User Installer
  • System Installer
  • .zip

特別な事情がない限り、「User Installer」を選んでおいて問題ありません。参考までに、インストール手段による違いを紹介します。

インストール方法 説明
User Installer アカウント毎に個別インストールする。
System Installer すべてのアカウントに一括インストールする。
.zip インストーラーを経由せず、展開先のディレクトリ内にファイルを保存する。

利用しているPCに複数のアカウントが存在する場合、「System Installer」からインストールすることですべてのアカウントがVSCodeを使えるようになります。

「.zip」はインストーラーを経由せず、圧縮ファイルから直接VSCodeをインストールします。設定ファイルなどはすべて解凍したディレクトリ内に保存されるため、外出先などで一時的にVSCodeを使いたい場合に最適です。

インストーラー経由でインストールすると、VSCodeのバージョンアップがあった場合に自動でアップデートを行ってくれます。長期的にVSCodeを使う場合は「User Installer」からインストールするようにしましょう。

インストーラーを使ってインストールする

ここからは、「Uset Installer」によるインストール手順を解説します。

Node.jsと違ってVSCodeのインストーラーは日本語化されているため、表示されている説明に従ってインストールしてください。

vscode-terms.png

vscode-folder.png

vscode-start-menu.png

vscode-add-task.png

上記の画面にある「PATHへの追加」は、コマンドプロンプトからVSCodeが開けるようになる機能です。その他の項目は。必要に応じてチェックしてください。もちろん、後から設定し直すことも可能です。

vscode-confirm.png

vscode-installing.png

vscode-completed.png

以上でインストール完了です。VSCodeの機能は多岐にわたり、ここですべてを紹介することはできません。日本語の情報も充実しているため、ぜひ使いながら覚えてくようにしてください。

以降のChapterでは、VSCodeの画面を参照しながらコードの解説をしていきます。

Lesson 2 Chapter 4
パッケージのインストール

Node.jsの特徴のひとつに、豊富なパッケージによる拡張性の高さが挙げられます。公式に提供されるパッケージはもちろん、有志によるオープンソース開発も盛んであり、多彩な機能を誰でも試すことが可能です。

こうしたパッケージは、npm(Node Package Manager)と呼ばれるツールで手軽にインストールできます。パッケージ間の依存関係を整理し、バージョンの競合問題を避けることもできるため、実際の開発では手放せないツールといえるでしょう。

今回は、このnpmの概要と使い方、および「package.json」と呼ばれるファイルについて学習します。

npmとは

npmは、Node.jsのパッケージを管理するためのツール(パッケージ管理システム)です。Node.jsの黎明期から続く歴史を持ち、現行ツールの中でも高いシェアを誇ります。

パッケージ管理システム

npmのようにパッケージのインストールやアンインストールを行うソフトウェアを、一般に「パッケージ管理システム(パッケージマネージャー)」と呼びます。
Linuxシステムの一部で使われている「apt」や、Pythonで使われている「pip」などもパッケージ管理システムのひとつです。

Node.jsは世界中の開発者に利用されており、オープンソースの様々なパッケージが公開されています。npmを介してこれらのパッケージを導入することで、自由度の高い開発が実現可能です。

パッケージ(モジュール)とは

ここで「パッケージ」という言葉について補足しておきましょう。Node.jsにおけるパッケージとは、「複数のJavaScriptモジュールをまとめたもの」を指します。

モジュールとは

外部のファイルから呼び出せる状態にした関数のこと。

通常、JavaScriptの関数は別のファイルから呼び出せません。外部から参照するためには、次のようなexport文を使って明示的にエクスポートする必要があります。

main.js
export const myFunc = function (num1, num2) {
  return num1 + num2
}

パッケージは、このエクスポートされたJavaScript関数を機能ごとにまとめたものです。多くはオープンソースとして公開され、誰でも手軽に利用することが可能です。

参考として、パッケージの具体例をいくつか挙げておきます。

  • 配列・文字列操作を行うためののパッケージ
  • httpリクエストを処理するためのパッケージ
  • 高速なWebフレームワークのパッケージ
  • 単体テストを作成するためのパッケージ

上記はあくまでも一例です。実際は100万以上のパッケージがnpm上で公開されており、自由にインストールすることが可能です。

ライブラリとの違い

ライブラリの定義もまた広範ですが、おおむねパッケージと同じ意味で使われています。 皆さんもを同じ意味に捉えて問題ありません。 傾向として、ネット上に公開されている外部パッケージを「ライブラリ」と呼ぶケースが多いです。

パッケージ管理の難しさ

ところで、そもそもなぜnpmのようなパッケージ管理システムが求められているのでしょうか。その理由は、パッケージの大規模化・複雑化にあります。

前述した通り、パッケージは複数のJavaScriptモジュールを内包しています。しかし、パッケージの数が増え、その規模が大きくなるにつれて、内部で他のパッケージを使用するパッケージも増えてきました。

以下のイメージ画像が示す通り、現在のパッケージといえば、他のパッケージに依存していることがほとんどです。

package.png パッケージのイメージ

このように大規模化・複雑化したパッケージでは、主として次の問題が発生します。

  • バージョン管理の問題
  • 依存関係の問題

いずれも手作業による環境構築を困難にするものです。順に解説していきましょう。

バージョン管理の問題

使用するパッケージの数が増えるにつれて、バージョン管理(バージョニング)の不具合が生じやすくなります。

バージョンとは

ファイルの更新状況を表す数字。バージョンとして振られた数字が大きいほど新しく、小さいほど古いファイルであることを意味します。

バージョニングの問題は、何もパッケージに限った話ではありません。ソフトウェア業界において、バージョン間の互換性には慎重な態度が求められます。

ひとつのソフトウェアをアップデートすることで、連動する他のソフトウェアが動作しなくなり、システム全体のダウンを引き起こす可能性があるからです。

そのため、ほとんどのソフトウェアはバージョンごとに互換性の有無を細かく定めています。PCや携帯のOSに付く「4.3.2」といった数字も、このルールに従ったものです。

依存関係の問題

複雑化したパッケージは、同時に依存関係の問題も引き起こします。

依存関係とは

あるプログラムを動かすために、別のプログラムが必要であること。

一言でいえば、依存関係とは「AファイルでBファイルのモジュールを使っている」ような状況です。モジュールがファイル間での呼び出しを前提としている以上、そこにはつねに依存関係が生じます。

module.png モジュールの利用

ごく単純なシステムであれば、Aファイルが依存するBファイルをインストールするだけで済みます。しかし、実際にはBファイルもCファイルに依存しており、CファイルもDファイルに依存する……といった複雑な状況が発生し得るでしょう。

パッケージ管理ツールによる自動化

このバージョン管理と依存関係の問題を解消するため、パッケージ管理ツールは開発されました。

Webアプリケーション開発では、外部パッケージを使う場面が多くあります(前述した通り、多くは「ライブラリ」と呼ばれます)。

外部パッケージは複雑な依存関係のもと構築されており、バージョンの互換性も様々です。手当たり次第に最新パッケージをインストールすれば良いという訳ではありません。正しい順番で、正しいバージョンのパッケージをインストールする必要があります。

二、三のパッケージならまだ管理できるでしょう。しかし、依存パッケージが10や100となると、現実的な管理はとても不可能です。

人的ミスも起きかねないため、npmのようなパッケージ管理システムが必要となるでしょう。

npmの使い方

それでは、実際にnpmを使う方法を見ていきましょう。

npmはNode.jsと同時に自動でインストールされ、コマンドラインから使用可能です。

コマンドラインとは

コマンドを用いてPCを操作できる画面のこと。Windowsだとパワーシェルやコマンドプロンプト、Macであればターミナルと呼ばれている。

試しにコマンドライン上にnpm -vと入力してください。以下のように何らかのバージョンが表示されれば、npmを利用できる環境は整っています。

コマンドライン(一例)
$ npm -v
6.14.12

npmでライブラリをインストールする

npmの操作は難しくありません。ライブラリをインストールする場合は以下のコマンドを打ちます。

コマンドライン
npm install ライブラリ名

たとえば、今後のLessonで学ぶライブラリ「Express」を入れる場合は以下のコマンドです。

コマンドライン
npm install express

この「express」というライブラリは、パッケージ間の複雑な依存関係によって構築されています。それらのパッケージをすべて手作業でインストールするのは大変です。

その点、npmであれば使用者が意識せず、自動で構築を行ってくれます。

npmでスクリプトを実行する

npmはパッケージ管理だけではなく、開発に必要なスクリプトを実行する機能もあります。

最もよく使われるのは、サーバーを立ち上げるためのスクリプトです。後述する「package.json」に任意のスクリプトを記述し、たとえばstartコマンドを割り当てます。

以降、コマンドラインから次のように入力するだけでサーバーの立ち上げが可能です。

コマンドライン
npm run start

他によく使われるものとしては、以下のスクリプトがあります。

  • 他にもパッケージの一覧を表示する
  • 開発プロジェクトの初期化を行う
  • 特定のファイルを実行する

上記の通り、npmはパッケージ管理だけではなく、開発に必要な機能の一部を担ってくれます。Node.jsを用いた開発には欠かせないツールと言っても過言ではありません。

package.jsonとは

Node.jsで必ず利用するファイルに「package.json」があります。主に使用するライブラリのバージョンが記載されているほか、前述したスクリプトの設定も行えるファイルです。

package.jsonを使う流れは以下の通りです。

  1. 以下の例のようにnpm installコマンドでライブラリをインストールする
  2. インストールされたライブラリのバージョン情報が自動で記載される
コマンドライン
npm install [ライブラリ名]

たとえば、Expressがインストールされた後のpackage.jsonは以下のようになります。"dependencies"の部分を見ると「Express」に対応する形で記載されています。

package.json
{
  "name": "sample",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  // ここに注目
  "dependencies": {
    "express": "^4.18.2"
  }
}

また、package.jsonにあらかじめライブラリとバージョンを記載しておき、一気にインストールすることも可能です。

その場合はライブラリ名を用いず、以下のコマンドで複数のライブラリをインストールできます。

コマンドライン
npm install

プロジェクトの作成

前回インストールしたVSCodeを使って、Chapter2「Node.jsを動かしてみる」で作成したディレクトリを開きましょう。

開始時のフォルダ構成
practice
 └─hello.js

上部の「File」メニューから「Open File」を選択します。

practice-top.png VSCodeの画面

画像下部に「ターミナル」と表示されているのは、VSCode内で立ち上げたコマンドラインです。通常のコマンドラインと操作は変わりません。

このように、VSCodeではコードの編集とコマンドラインの実行を同じ画面で行うことが可能です。

package.jsonの作成

続いて、プロジェクトの初期設定を行います。カレントディレクトリを「practice」に変更し、以下のコマンドを入力してください。

コマンドプロンプト
npm init -y

実行すると、カレントディレクトリに「package.json」というファイルが作成されているはずです。

practice-installed.png コード実行後のVSCodeの画面

前述した通り、「package.json」ファイル内のdependencies以下には、インストールしたパッケージの情報が書き込まれます。現在はnpmでパッケージをインストールしていないため、何も記述されていない状態です。

参考:パッケージのインストール方法

パッケージは、ターミナルでnpm install <package名>を実行することでインストールできます。 ただし、インストール時に以下のオプションを指定できます。

コマンド インストール種類 使うタイミング
npm install -g <package名> グローバルインストール 現在開発中のプロジェクト以外でも使うパッケージの場合
npm install -D <package名> ローカルインストール ・開発中のプロジェクトのみで使うパッケージの場合
・開発環境のみで使うパッケージの場合
npm install <package名> ローカルインストール ・開発中のプロジェクトのみで使うパッケージの場合
・本番環境でも使うパッケージ場合

※開発環境:文字通りプロジェクトの開発を行う環境

※本番環境:実際にユーザーがシステムに触れる環境

開発環境と本番環境の違いについては、この後のLesson12「複数の環境に適用する」で詳しく扱います。現時点では、環境ごとにパッケージを使い分けられることを気に留めておきましょう。

まとめ

今回はNode.jsのパッケージ管理システム「npm」について学びました。

npmを用いることで、面倒なバージョン管理や依存関係の問題から解放されます。また、開発に必要なスクリプトを実行できるなど、パッケージ管理以外の機能も豊富です。

npmの各種設定は「package.json」に記述されます。"dependencies"以下にあるパッケージ名とバージョンを確認するようにしてください。

特に重要なこと

  • npmはNode.jsのパッケージ管理システムであり、パッケージ間の依存関係を解決する
  • npmは開発のために便利な機能を備えており、コマンドラインで利用することができる

次のChapterでは、Node.jsを使って実際にブラウザ上に文字を表示させてみます。

Lesson 2 Chapter 5
Hello Worldをブラウザに表示する

前回のChapterでは「package.json」の内容を解説しました。それでは、実際にコードを記述し、ブラウザに「Hello World!」の文字を表示させてみましょう。

main.jsの作成

まずはVSCodeを開いて、作業用の新しいファイルを作成します(左側の「エクスプローラー」ビューから直接ディレクトリを参照できます)。

helloWorld-new-file.png

helloWorld-create-main.png

画像の通り、作業ディレクトリの直下に「main.js」というファイルを作成しました。

続いて、作成した「main.js」ファイルに以下のコードを書いてみましょう。

main.js
const http = require('http');

var server = http.createServer(
  (request,response)=>{
    response.end('Hello World!');
  }
)
server.listen(3000);

main.jsの起動

コードの説明は後ほど行います。VSCodeのターミナルで以下の通り入力し、Node.jsを実行しましょう。

ターミナルの表示

VSCodeにターミナルが表示されていない場合は、「ターミナル」メニューから「新しいターミナル」を選択してください。

ターミナル
node main.js

その後、任意のブラウザで「localhost:3000」にアクセスしてください。 以下の画像のように表示されたら成功です。

helloWorld-display.png

コードの解説

先ほどの「main.js」について、具体的なコードを見ていきましょう。

main.js
const http = require('http');

上記の部分では、HTTPにアクセスするための「httpモジュール」を読み込み、定数httpに保管しています。

main.js
var server = http.createServer(
  (request,response)=>{
    response.end('Hello World!');
  }
)

上記の部分では、クライアントからのアクセス(リクエスト)に対するレスポンスを記述しています。

今回の場合、レスポンスとして「Hello World!」の文字列を出力する指定です。

main.js
server.listen(3000);

最後の箇所でサーバーのポート番号を指定しています。

以上でLessson2は完了です。Node.jsの環境構築を無事に終え、簡単なコードを実行することができました。

次のLesson3では、EJSとExpressを使って動的な画面を表示させてみましょう。

Lesson 2 Chapter 6
NestJSのインストール

NestJSのインストールを行います。

NestJS CLIツール

NestJSにはCLIツールが存在します。NestJSの環境構築はこのCLIツールをインストールするだけで完了します。

CLI

CLIツールとはコマンドラインでNestJSの新規プロジェクトを作成したり、ファイルを指定の場所に作成してくれたりアプリケーション開発を行うとき便利なコマンドを提供してくれるソフトウェアです。

インストール

下記コマンドでインストールしましょう。環境へグローバルにインストールするためディレクトリはどこにいても構いません。

ターミナル
npm i -g @nestjs/cli

インストールされたか確認するために下記コマンドでバージョンを確認しましょう。

ターミナル
nest --version

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

NestJSプロジェクトを立ち上げる

今回構築していくプロジェクトを立ち上げておきます。プロジェクトの立ち上げは下記コマンドで実行できます。ディレクトリは任意の作業ディレクトリで行いましょう。「nest-todo」はプロジェクト名なので好きな名前にしても結構です。

ターミナル
nest new nest-todo

使用するパッケージマネージャーを聞かれます。「npm」を選択しましょう。

terminal_lesson3-ch1.png npmを選択

その後、しばらく待って下記のように表示されれば成功です。

terminal_lesson3-ch1-2.png プロジェクト作成完了

プロジェクトを起動する

プロジェクトの作成が完了したので、起動します。プロジェクト配下に移動します。

ターミナル
cd nest-todo

下記コマンドで起動できます。

~/nest-todo
npm run start:dev

サーバーが立ち上がるのでブラウザで「http://localhost:3000/」へアクセスしましょう。「Hello World!」と表示されれば成功です。これでNestJSのインストールは以上になります。

Lesson 2 Chapter 7
TypeORMのインストール

TypeORMのインストールを行います。TypeORMNodejs専用のORMです。

ORM

ORMは「オブジェクトリレーショナルマッパー(マッピング)」の略です。DBのデータ構造とオブジェクト指向のデータ構造をマッピングし、オブジェクト指向言語のメソッドを使ってDB操作を可能にしてくれる仕組みを提供してくれる技術のことです。

TypeORMのインストール

「Lesson3 Chapter1 NestJSのインストール」で作成したプロジェクト直下に移動しましょう。

ターミナル
cd nest-todo

TypeORMを使用するには以下をインストールする必要があります。

ターミナル
npm install @nestjs/typeorm typeorm mysql2

ピュアなTypeORM以外にNestJS用へ調整された@nestjs/typeormとMySQLへ接続するためのmysql2をともにインストールします。

こちら公式サイトにもインストールと導入手順が記載されているので参考にして見てください。これでTypeORMのインストールは完了です。

TypeORM Integration

Lesson 2 Chapter 8
Thunder Clientのインストール

Thunder Client

Thunder Clientとはサーバーに対してリクエストを送ることができるVScodeの拡張機能です。

サーバーに対してのリクエストというのは、たとえば「http://localhost:3000/」へのアクセスも含まれます。このアクセスは「サーバーで待ち構えているAPIに対してのGETリクエスト」になります。

API

APIとは「アプリケーションプログラミングインタフェース」の略で、プログラムを用いてアプリケーションの機能を利用する仕組みのことをいいます。と言われてもピンとこない人が多いと思います。

私たちは知らぬうちAPIを利用しています。私たちが使っているアプリケーションは私たちのアクションに合わせてサーバーにリクエストを送信し、APIとして待ち構えている関数に対して処理を要求します。その関数はDBのデータを操作したり、何か必要な情報を返してくれたりします。

つまりAPIとはバックエンドの関数のことを指します。私たちがこれからNestJSを用いて実装していくものです。

Thunder Clientのインストール

Thunder ClientはVScodeの拡張機能なのでnpmは使わずVScodeでインストールします。

  1. VScodeのエクスプローラーを開きましょう。Macの方は「command + B」、Windowsの方は「Ctrl + B」で開けます。左にサイドバー開けば成功です。
  2. VScodeのマーケットプレイスにアクセスしましょう。サイドバーのアイコンにマウスをホバーしていき「拡張機能」と書かれたアイコンをクリックしましょう。
  3. vscode_ch3-1.png 拡張機能

  4. 入力欄に「thunder」と入力しましょう。「インストール」を押しましょう。
  5. vscode_ch3-2.png Thunder Client

  6. インストールが済んだらサイドバーの一番下にアイコンが追加されたのを確認しましょう。
  7. vscode_ch3-3.png Thunder Client アイコン

実際に叩いてみる

インストールが終わったので実際にAPIを叩いてみましょう。※「npm run start:dev」でNestJSのサーバーは立ち上げておいてください。

1. サイドバーの「Thunder Client」アイコンをクリックする

2. 「New Request」ボタンをクリックする

thunder_ch3-1.png

3. URLを指定する

thunder_ch3-2.png

4. 「Send」を押下するとレスポンスが表示される

thunder_ch3-3.png

まとめ

今回はAPIの説明とThunder Clientのインストールを行いました。APIは難しい概念ではありますが使いながら覚えていきましょう。

Lesson 2 Chapter 9
TypeScriptとは

ここまで、Nest.jsの環境構築が完了しました。Lesson1のChapter3でも解説している通り、Nest.jsはTypeScriptで作られTypeScriptが標準仕様となっています。 そのため、実際にNest.jsを動かして行く前にTypeScriptについて学習していきましょう。

TypeScriptとは

TypeScriptは、Microsoftによって開発されました。TypeScriptは、JavaScriptの主要な問題点の一つである型の不一致によるエラーを解決するために設計された、静的型付け言語です。 静的型付けを導入することにより、コードの品質や保守性が向上し、バグの発生を減らすことができます。以降のChapterでTypeScriptについて、Lesson3よりNest.jsについて学ぶことによってTypeScriptを採用することによってなぜコードの品質や保守性が向上するのかを理解しましょう。

TypeScriptの特徴

TypeScriptの主な特徴は以下の通りです

  • 静的型付け
  • 値や変数に型を明示的に指定することができます。これにより、コンパイル時に型エラーを検出することができます。

  • オブジェクト指向プログラミング
  • クラスやインターフェースのサポートにより、オブジェクト指向プログラミングのパターンを使用することができます。

  • 最新のJavaScript機能のサポート
  • TypeScriptは、ECMAScriptの最新の機能をサポートしています。JavaScriptの新機能を利用しながら、静的型チェックの恩恵を受けることができます。

  • IDEやエディタのサポート
  • TypeScriptは、強力な型推論やエラーチェックの機能を持つため、IDEやエディタでの開発サポートが充実しています。コードの補完やリファクタリングなどの機能が利用できます。

  • ツールエコシステムの豊富さ
  • TypeScriptは、広く採用されているため、豊富なツールやライブラリが利用できます。また、型定義ファイルを使用することで、既存のJavaScriptライブラリを型安全に利用することができます。

    まとめ

    このような特徴によりTypeScriptは、Webアプリケーションやサーバーサイドの開発、モバイルアプリ開発など、さまざまな領域で使用されています。それでは早速TypeScriptについて学んでいきましょう。まずは型推論からです。

    Lesson 2 Chapter 10
    型推論

    型推論とは

    型推論とは、変数の型に型注釈がない場合にコンパイラが型を自動で判別する機能です。 型注釈とは、変数に代入可能な値の型を開発者が指定することを言います。 次の例は型推論、型注釈がどのようなものかを示しています。

    変数num1には数値の123が代入されています。

    let num1 = 123;

    次のようにnum1をホバーした時に右上に表示されるものが推論された型です。

    let1.png

    let num1: number と表示されています。numberは数値型を表します。 変数num1は数値型に型推論されていると言えます。

    次に型注釈を見てみます。

    let num1: number = 123;

    変数num1には: numberが付与されています。 変数にコロンを使い:型と付与することで変数の型を指定できます。 このように型を明示的に指定することを型注釈と言います。

    本章ではconst、let、Array、Tuple、Promise、JSONデータについて、 どのように型推論されるのかを中心に説明をしていきます。

    情報

    コンパイラとは、プログラミング言語を機械語、もしくは機械語に近い中間言語に変換するプログラムです。 プログラミング言語は人間の思考に近い機能や構文を持つ言語であるのに対し、 機械語はコンピュータの中央処理装置が理解できる数字の0か1を並べた2進数になります。
    TypeScriptにおいては、コンパイラは2つの機能を示します。 1つは型チェックの機能、もう1つはTypeScriptコードをJavaScriptコードに変換する機能です。

    constの型推論

    constで宣言した変数num1は数値の123が代入されています。

    const num1 = 123;

    const6.png

    変数num1は数値のリテラル型123と型推論されます。 リテラル型とはプリミティブ型の特定の値だけを代入可能にする型です。 例えば、真偽値型のtrueとfalse、数値型の値、文字列型の文字列です。

    次にnum1をletで宣言した変数num2に代入してます。

    let num2 = num1;

    const2.png

    変数num2をホバーして型推論を見てみます。 変数num2は数値型と型推論されています。 constで宣言したリテラル型の変数を再代入可能な別の変数に代入するとプリミティブ型に変換されます。 プリミティブ型とは、真偽値型、数値型、文字列型、undefined型、null型、シンボル型、bigint型の7つの型になります。 リテラル型からプリミティブ型に自動で型が変換される挙動をリテラル型のWideningと言います。 num1は数値のリテラル型でしたが、num1を代入したnum2はWideningにより数値型と型推論されます。

    情報

    constを使って変数を宣言すると、変数の値をイミュータブルにすることができます。 イミュータブルとは、オブジェクト作成後に作成されたオブジェクトの状態を変えることができないという意味です。 つまりconstで定義した変数の値を書き換えることはできません。 constを使う場合、初期値は必須になります。

    letの型推論

    letで宣言した変数は値の再代入ができます。 初期値の定義は任意になります。

    初期値を代入しなかった場合の型推論を見てみます。 次のように初期値を代入しない変数num1を定義します。

    let num1;

    let2.png

    変数num1の型はany型と型推論されます。

    次に変数num1に123を代入します。

    let num1 = 123;

    let1.png

    変数num1をホバーして型推論を見てみます。 変数num1の型は数値型と型推論されています。 変数num1にはリテラル型を代入しています。なぜ変数num1はリテラル型の123と型推論されないのでしょうか。 これは再代入可能な変数の型がリテラル型に推論されそうになるとリテラル型からプリミティブ型に型が変換される挙動が発生するからです。 リテラル型からプリミティブ型に自動で型が変換される挙動をリテラル型のWideningと言います。 つまりWideningにより数値型と型推論されたということになります。

    情報

    any型はどのような値にも代入でき、どのような値としても使えます。 any型を持った値はTypeScriptの型チェックによるエラーが発生しません。 TypeScriptによる型の安全が保証されないことになります。 このため、any型を使うことはできるだけ避けるべきです。

    Arrayの型推論

    配列を宣言するにはブラケットを使います。 ブラケットとは、[]のことです。 配列は要素の集まりです。 ブラケットの中に要素が連なっています。 型注釈せずに配列を宣言した場合、初期要素のデータ型から配列の型が型推論されます。

    次の変数arrは初期要素を持たない配列です。 初期要素を持たない配列はany型と型推論されます。

    const arr = [];

    array1.png

    any型を使うことはできるだけ避けるべきです。 例えば文字列型の配列型を定義する場合、次のように型注釈します。

    const arr: string[] = [];

    次に複数のプロパティを持つ配列型の型推論を見てみます。 変数arr2は文字列型のプロパティを3つ持つ配列型のオブジェクトです。

    const arr2 = [
    { name: 'Tom'},
    { name: 'John'},
    { name: 'Mike'}
    ];

    変数arr2は文字列型のnameプロパティを持つ配列型のオブジェクトと型推論されます。

    array3.png

    情報

    配列型には別の書き方があります。 変数名: Arrayと定義します。 先に学習した: 型名[]と意味の違いはありません。 例えばarr2を型注釈する場合、次のようになります。

    const arr2: Array = [
    { name: 'Tom'},
    { name: 'John'},
    { name: 'Mike'}
    ];

    Tupleの型推論

    タプル型はブラケットに入る要素の型を明示的に宣言する必要があります。 用途としては、例えば文字列型、数値型の順で要素を持たせたい場合にタプル型を宣言します。

    次の変数tupleStringNumberは、文字列型の'Hello'と数値型の2を持つ配列型です。

    let tupleStringNumber = ['Hello', 2];

    tuple1.png

    変数tupleStringNumberは 文字列型と数値型を持つユニオン型の配列型と型推論されます。

    変数tupleStringNumberは、文字列型、数値型の順序を持つことが期待されます。 しかし、ユニオン型の配列型と型推論されるため、 次のように数値型、文字列型の順で値が再代入されてしまいます。

    tupleStringNumber = [33, 'こんにちは'];

    これは意図しない型の順序になります。 そこで、次のように明示的に型を宣言することで型の順序を指定することができます。

    let tupleStringNumber: [string, number] = ['Hello', 2];
    
    let tupleStringNumber = ['Hello', 2] as [string, number];

    [string, number]というように、ブラケットの中に並べたい順で型を指定します。 上の1つ目の例では変数にタプル型を型注釈しています。 2つ目の例ではasを使って型を上書きしています。asについては第5章の型キャスト項で説明します。

    Promiseの型推論

    Promise型とは、Promiseインスタンスを返す関数の戻り値の型になります。

    次の関数promiseResolveは、引数に文字列型のnameを受け取りresolveの結果をPrimiseインスタンスとして返す関数です。

    function promiseResolve(name: string) {
    return new Promise(resolve => {
    setTimeout(() => {
    resolve(name);
    }, 10);
    })
    };

    戻り値の型について型推論を見てみます。

    promise8.png

    関数promiseResolveは、resolveメソッドが呼び出された場合、引数で受け取った文字列を返します。 一見、文字列型と型推論されると見て取れそうです。しかし、Promise<unknown>というように Promise型の型引数はunknown型と型推論されます。 なぜ戻り値の型引数は文字列型に型推論されないのでしょうか。 Promise型の型引数がunknown型と型推論されることについて、Promise型の型引数の指定方法を確認しながら説明します。

    なお、Promise型は型引数のエイリアスにジェネリクスを利用しています。 ジェネリクスについてはジェネリクスの章で説明します。

    情報

    型引数とは、型を定義するときにパラメータを持たせることができるというものです。 例えば関数を定義するときに関数名に続けて指定します。 関数名<T>というように<>で型を囲って指定します。

    まず、Promise型の型引数の指定方法については、次の2つの方法があります。

  • promiseResolve関数の戻り値の型としてpromise型の型引数を指定する
  • new PromiseのPromiseインスタンスに型引数を指定する
  • どちらか一方に型引数を指定することでPromise型の型引数を指定することができます。 しかし、関数promiseResolveはどちらにも型引数を指定していません。

    戻り値の型とnew PromiseのPromise型の2つの型推論について見てみます。 関数promiseResolveの戻り値の型については、Promise項の冒頭で既に型推論の結果を確認しました。 もう一方のPromiseインスタンスの型について型推論を見てみます。 次のようにnew PromiseのPromise型をホバーします。

    promise1.png

    promise5.png

    Promise型の型引数unknown型はコンストラクタ関数内にあるresolve関数の引数の型として渡されています。 resolveされた場合、戻り値のPromise型の型引数としてunknown型が渡されていることが確認できます。

    Promise型の型引数は、コンストラクタ関数内のresolve関数の型引数を指定するために使われています。 つまり、関数promiseResolveの戻り値の型引数がunknown型になっていたのは、Promise型の型引数を指定していなかったためです。 Promise型の型引数を指定していない場合、resolve関数はunknown型の型引数を受け取ります。 resolveの結果を受け取ったPromiseインスタンスの型引数がunknown型になり、戻り値の型引数として返されたという訳です。

    次の2つの例は、どちらもPromise型に文字列型を型注釈した例になります。

    function promiseResolve(name: string): Promise<string> {
    return new Promise(resolve => {
    setTimeout(() => {
    resolve(name);
    }, 10);
    })
    };
    関数promiseResolveの戻り値の型にPromise型を型注釈

    function promiseResolve(name: string) {
    return new Promise<string>(resolve => {
    setTimeout(() => {
    resolve(name);
    }, 10);
    })
    };
    new PromiseのPromise型に型注釈

    関数promiseResolveの戻り値のPromise型に文字列型を型引数として指定した場合、 コンストラクタ関数内にあるresolve関数の型引数に文字列型が渡されます。 resolveの結果を受け取ったPromiseインスタンスの型引数は文字列型になります。 戻り値の型引数として文字列型が返されていることが確認できます。

    promise4.png

    promise9.png

    Promiseの理解について

    以下では、Promiseを使った非同期処理、Promiseでのエラーハンドリングについて説明しています。 Promiseを使った非同期処理は、Promiseの型推論及びasync/awaitの型推論の理解を深めるために役立つ知識となります。 なお、Promiseの理解が進んでいる方は読み飛ばしてasync/awaitの型推論項から学習を継続していただいても問題はありません。

    情報

    Promiseを使った非同期処理

    Promiseは非同期処理の結果を表すビルトインオブジェクトです。 Promiseはnew演算子でPromiseをインスタンス化して利用します。 Promiseがインスタンス化されるとexecutor関数が呼ばれます。 executor関数の中で非同期処理が行われます。 非同期処理が解決された場合はexecutor関数のresolve関数、拒否された場合はreject関数が呼ばれます。 resolve関数が呼ばれた場合、resolveの結果がpromiseインスタンスに渡され、インスタンスが解決した状態に変わります。 reject関数が呼ばれた場合、rejectのエラーがpromiseインスタンスに渡され、インスタンスが拒否された状態に変わります。 resolveの結果、もしくはrejectのエラーが渡されるとPromiseインスタンスはコールバック関数を呼び出します。 Promiseが解決された状態の場合、thenメソッドが呼ばれます。

    promise2.png

    thenメソッドに処理の結果が渡されると、thenメソッドが引数に受け取るコールバック関数が呼ばれます。 コールバック関数にはthenメソッドに渡された処理の結果が引数として渡されます。 コールバック関数に登録された処理が実行され値を返します。

    promise3.png

    Promise.thenの処理では以下の2つの処理が実行されています。

    • 非同期処理を行うexecutor関数は非同期処理が完了したらPromiseインスタンスを返す
    • 返されたPromiseインスタンスをthenメソッドが受け取りthenメソッドのコールバック関数に渡す

    次の例はPromise.thenを利用した非同期処理の例です。 関数promiseResolveは引数に文字列型のnameを受け取ります。 非同期処理が行われ解決された場合、resolve関数を呼び出します。 resolve関数が呼ばれた場合、resolveの結果がpromiseインスタンスに渡され、インスタンスが解決した状態に変わります。

    function promiseResolve (name: string) {
    return new Promise(resolve => {
    setTimeout(() => {
    resolve(name);
    }, 10);
    })
    };

    次のように関数promiseResolveの引数に文字列'Tom'を渡し実行します。 関数promiseResolveにはthenメソッドを追加しています。

    promiseResolve('Tom').then(result => console.log(result));

    promise6.png

    Promiseインスタンスが解決された状態の場合、thenメソッドが呼ばれます。 thenメソッドに処理の結果が渡されるとthenメソッドのコールバック関数resultが呼ばれます。 thenメソッドに渡された処理の結果をコールバック関数resultが受け取ります。 結果を受け取りコンソールに表示します。 resolveには関数promiseResolveが引数で受け取った文字列の'Tom'がセットされています。 コンソールから確認すると'Tom'が表示されます。

    情報

    Promiseでのエラーハンドリング

    Promiseが拒否された状態の場合、Promiseチェーンにcatchメソッドを追加するとエラーのハンドリングができます。 rejectのエラーをcatchメソッドが受け取ります。 catchメソッドはエラーを表示するといった処理を実行します。

    次の関数somethingFetchは引数で受け取った文字列が'ok'で始まる文字列かどうかを調べ結果を返します。 rejectされた場合、'不適切な文字列です'のメッセージを含んだErrorオブジェクトを生成します。 関数somethingFetchはrejectされた場合のエラーをPromiseインスタンスとして返します。

    function somethingFetch(path: string) {
    return new Promise((resolve, reject) => {
    setTimeout(() => {
    if (path.startsWith('ok')) {
    resolve(`${path}で始まる文字列です。`);
    } else {
    reject(new Error('不適切な文字列です'));
    }
    }, 1000);
    });
    }

    引数に'k'を渡して関数somethingFetchを実行します。 関数somethingFetchにはPromiseチェーンを使用してcatchメソッドを追加しています。

    somethingFetch('k').then(result => {
    console.log(result);
    }).catch(error => {
    console.log(`失敗しました。${error}`);
    });

    promise-error1.png

    'k'は成功時の条件を満たさずrejectされた場合のPromiseインスタンスが返されます。 rejectされたPromiseインスタンスをcatchメソッドが受け取ります。 catchメソッドにはエラー時の処理を実行するコールバック関数errorが指定されています。 コールバック関数errorはcatchメソッドからPromiseインスタスを受け取ります。 登録したエラー時の処理を実行し`失敗しました。${error}`というエラーメッセージをコンソールに表示します。 関数errorにはPromiseインスタンスから受け取った'不適切な文字列です'という値がセットされています。 コンソールの出力結果から、 エラーをハンドリングができていることが確認できます。

    エラーのハンドリング方法として、thenメソッドに第2引数を指定する方法があります。 thenメソッドの第2引数にコールバック関数を指定してエラー時の処理を登録できます。 関数somethingFetchを使用します。

    引数に'k'を渡して関数somethingFetchを実行します。

    somethingFetch('k').then(
    result => {
    console.log(result);
    }, error => {
    console.log(`失敗しました。${error}`)
    });

    promise-error1.png

    'k'は成功時の条件を満たさずrejectされた場合のPromiseインスタンスが返されます。 thenメソッドの第2引数には、エラー時の処理を実行するコールバック関数errorが指定されています。 thenメソッドはrejectされた場合のPromiseインスタンスを受け取ります。 コールバック関数errorはthenメソッドからPromiseインスタスを受け取ります。 登録したエラー時の処理を実行し`失敗しました。${error}`というエラーメッセージをコンソールに表示します。 関数errorにはPromiseインスタンスから受け取った'不適切な文字列です'という値がセットされています。 コンソールの出力結果から、 エラーをハンドリングができていることが確認できます。

    Promiseがrejectされた時にコールバック関数が登録されていない場合、どのような結果になるのかを確認します。 次のようにerrorメソッドを削除し引数に'k'を渡して関数somethingFetchを実行します。

    somethingFetch('k').then(
    result => {
    console.log(result);
    });

    promise-error2.png

    UnhandledPromiseRejectionエラーが発生します。 エラーがハンドリングされずrejectされたままの状態を示す警告です。 エラーによりプロセスが強制終了します。 例えば、使っていたアプリが動かなくなるといったことにつながります。 適切なエラーのハンドリングを追加しプロセスの強制終了を回避する必要があります。

    async/awaitの型推論

    Promiseをベースとした非同期処理として、async関数とそのブロック内で使うawait式からなるasync/awaitがあります。 Promiseインスタンスを返す関数とasync関数を定義して、async関数の戻り値の型について型推論を見てみましょう。

    次の関数sampleResolveはPromiseインスタンスを返す関数です。数値型の引数を受け取ります。

    function sampleResolve(value: number) {
    const promise = new Promise(resolve => {
    setTimeout(() => {
    resolve(value);
    }, 10);
    });
    };

    resolve関数が呼ばれた場合、resolveの結果がpromiseインスタンスに渡され、インスタンスが解決した状態に変わります。 解決した状態とresolveした値をPomiseインスタンスとして返します。 戻り値の型に型注釈はしていません。

    次の関数sampleはasync/awaitを利用しています。 関数sampleは数値型の引数を受け取ります。

    async function sample(value: number) {
    return await sampleResolve(value);
    };

    awaitを利用して関数sampleResolveからPromiseインスタンスの結果が返されるまで処理を待ちます。 戻り値の型に型注釈はしていません。

    実際に関数sampleに数値の2を渡して実行してみます。 関数sampleにはthenメソッドを追加しています。

    sample(2).then(result => {
    console.log(result);
    });

    promise7.png

    関数sampleからPromiseインスタンスの処理の結果をthenメソッドで受け取ります。 Promiseインスタンスにはresolveの結果として数値の2がセットされています。 thenメソッドのコールバック関数resultが呼び出されます。 resultは受け取ったresolveの値を表示します。 コンソールから確認すると2が表示されます。

    次のようにasync/awaitを利用した関数sampleの戻り値の型を見てみます。

    promise-async3.png

    Promise型の型引数はunknown型と型推論されます。

    先に述べた通り、関数sampleResolveの戻り値の型は型注釈されていません。 したがって関数sampleResolveのPromise型の型引数はunkonwn型となります。 関数sampleにはunknown型のPromiseインスタンスが返されます。 結果、関数sampleのPromise型の型引数はunkonwn型と型推論されます。

    関数sampleResolveから返されるPromiseインスタンスの型は数値型です。 次のように関数sampleResolveの戻り値の型をPromise<number>と型注釈します。

    function sampleResolve(value: number): Promise<number> {
    const promise = new Promise(resolve => {
    setTimeout(() => {
    resolve(value);
    }, 10);
    });
    };

    関数sampleResolveから返されたPromiseインスタンスの型が数値型に型注釈されたため 関数sampleのPromise型の型引数は数値型に型推論されます。

    promise-async4.png

    情報

    関数をasyncを使い宣言すると関数の戻り値はpromiseになります。
    await式はasync関数内で使います。awaitによりpromiseの結果が返されるまで処理を待機させます。

    JSONデータの型推論

    関数getRequestは郵便番号から所在地データを取得する関数です。

    const axios = require('axios');
    
    async function getRequest() {
    const url = 'https://zipcloud.ibsnet.co.jp/api/search?zipcode=1500045';
    
    try {
    const results = (await axios.get(url)).data.results;
    console.log(results);
    } catch (error) {
    console.error(error);
    }
    }

    tryブロック内でデータが取得できるかを試しています。 エラーがなければ取得した所在地データは変数resultsに格納されます。 変数resultsをコンソールから表示します。

    次のように関数getRequestを実行します。

    getRequest();

    josn3.png

    コンソールから確認すると変数resultsが持つ値が表示されます。 取得した値はJSONデータであるとわかります。

    次のようにresultsをホバーして 変数resultsの型推論を見てみます。

    json2.png

    変数resultsの型はany型と型推論されています。 any型を許容すると型の安全性が損なわれるため使用は極力避けるべきです。 このような場合、JSONデータとデータ構造が一致する型を新たに定義して型注釈します。

    JSONデータを参照してプロパティ名とプロパティの型が一致する新たなResultsType型を宣言します。 例えば、address1の値は文字列型です。 address1: string;というようにプロパティを作成します。

    interface ResultsType {
    address1: string;
    address2: string;
    address3: string;
    kana1: string;
    kana2: string;
    kana3: string;
    prefcode: string;
    zipcode: string;
    }

    次のように変数resultsにResultsType型を型注釈します。

    const results: ResultsType = (await axios.get(url)).data.results;

    josn1.png

    外部APIから取得する値について、値の取得時はTypeScriptの型チェックは機能しません。 値の取得は実行時に行われるためです。 例えば、次のようにResultsType型のプロパティをJSONデータの構造と一致しないデータ構造に変更します。

    interface ResultsType {
    address1: number;
    address2: string;
    address3: string;
    }

    TypeScriptは外部APIからどのようなデータ構造の値が取得できるかわからないため コンパイルエラーは発生しません。 また、実行時のランタイムエラーも発生しません。

    しかし、型の安全が損なわれないよう、any型を許容することや型が一致しない型注釈をすることは避けるべきです。 外部APIから取得したデータ構造はバージョンアップなどで変更になる可能性があります。 JSONデータとデータ構造が一致する型を定義して、Partial型で受けるといった工夫や 定期メンテナスに型のチェックを含むなど、プロジェクトの特性に応じて適切な対応をしてください。

    情報

    axiosとは、HTTP通信によってデータの更新・取得を行うことができるJavaScriptのライブラリです。 APIを提供するクラウドサービスに対して、データを送受信することができます。
    HTTP通信は、例えばユーザがインターネットを利用して検索する場合、ブラウザにデータを入力して送信します。 送信したデータはサーバが受け取り、関連する情報を返します。 このデータを送受信するための通信がHTTP通信といったものになります。
    ライブラリは、利便性を向上させるようなプログラムを 他のプログラムが引用できる状態にしたものです。

    Lesson 2 Chapter 11
    型安全

    型安全とは、型チェックにより不正なコードをコンパイル時に検出するといった、 TypeScriptによる型の安全が保証されていることを表しています。 本章では、 キャストにより型の安全を失う要因、型ガードによる安全な型の取り扱い、型の互換性 について説明をしていきます。

    型キャスト

    キャストとは、型の互換性に基づき型を変換することです。 プログラマがTypeScriptの型推論より抽象度が高い型で型を上書きすることや、抽象度が低い型で型を上書きすることができます。

    抽象度が高い型、低い型について説明します。 例えば無関係に定義された2つの型、型A、型Bがあるとします。 型Aと型Bのオブジェクト型を比較し、型Aが型Bのオブジェクト型の条件を満たす場合、型Aは型Bの部分型であると言えます。 部分型とは構造的に型が一致するのであれば、サブタイプとして扱うというTypeScriptの機能です。 サブタイプは、スーパータイプから派生したより多くの要素を持つ具象化された型であり、 スーパータイプに比べ抽象度が低い型になります。 一方、スーパータイプはサブタイプより要素が少なくシンプルなオブジェクト型になるため、 サブタイプに比べ抽象度が高いと言えます。 つまり、型Aは型Bのサブタイプ、型Bは型Aのスーパータイプになります。 型Aは型Bに比べ抽象度が低い型と言え、型Bは型Aに比べ抽象度が高い型と言えます。

    情報

    intro1.png スーパータイプとサブタイプの関係と抽象度

    サブタイプはスーパータイプに代入可能です。 型Aは型Bに代入することができます。 型の互換性とは、型が代入可能であることを表します。 具体例については本章末の型の互換性項で紹介します。

    型を上書きすることはTypeScriptによる型安全の保証が得られなくります。 以下では、

    • 型推論より抽象度が高い型に上書きした場合の危険性
    • 型推論より抽象度が低い型に上書きした場合の危険性

    について紹介します。

    情報

    型推論とは、変数の型に型注釈がない場合、コンパイラが型を自動で判別する機能です。 型注釈とは変数に代入可能な値の型を開発者が指定することを言います。

    型推論より抽象度が高い型に上書きした場合の危険性

    次の例は、文字列型に型推論された値を文字列型よりも抽象度が高いany型に上書きした例です。

    関数stringToAnyは文字列型の引数を受け取り、受け取った値を返します。戻り値の型注釈はありません。

    function stringToAny(value: string) {
    return value;
    };

    cast6.png

    関数stringToAnyは引数に文字列型を受け取るため、戻り値の型は文字列型に型推論されます。

    まず、次のように戻り値の型に文字列型よりも型の抽象度が高いany型を型注釈します。

    
    function stringToAny(value: string): any {
    return value;
    };

    次に、関数stringToAnyに文字列の'1.234'を渡して、戻り値を変数toNumberに格納します。 変数toNumberは数値型に型注釈しています。

    const toNumber: number = stringToAny('1.234');

    関数stringToAnyの戻り値はany型です。 any型はどのような値の型としても利用できるため 型チェックが機能しません。 このため数値型の変数toNumberに格納されてしまいます。

    次に、変数toNumberからtoFixedメソッドを呼び出します。

    toNumber.toFixed();

    toFixedメソッドはNumberオブジェクトのプロトタイプメソッドです。数値型のみ利用できます。

    型はコンパイル時に削除されます。 型付けは値そのものを書き換えることはありません。 変数toNumberには文字列が格納されています。 JavaScripコードの実行時はランタイムエラーが発生します。

    cast3.png

    情報

    プロトタイプメソッドとは、Objectに組み込まれているメソッドのことです。 ObjectはJavaScriptのデータ型の一つです。JavaScriptのほぼすべてのオブジェクトは Objectを継承しています。

    型推論より抽象度が低い型に上書きした場合の危険性

    抽象度が低い型に型推論を上書きする場合、型アサーションを使います。 型アサーションは、式as型という構文が用いられます。 コンパイル時に式の型を強制的にasで指定した型に上書きします。

    次の例は、型アサーションを使いプログラマの意図する型に型を上書きした例になります。

    変数profileはプロパティを持たない空のオブジェクト型です。

    let profile = {};

    変数profileに、nameプロパティとして'Tom'、ageプロパティとして33を代入しようとするとコンパイルエラーが発生します。

    profile.name = 'Tom';
    profile.age = 33; 

    assertion1.png

    TypeScriptが変数profileを空のオブジェクト型と型推論したためプロパティを代入できません。

    次のようにnameとageの2つのプロパティを持つTypeProfile型を定義します。

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

    変数profileに型アサーションを使いTypeProfile型を指定します。

    let profile = {} as TypeProfile;

    型アサーションにより型推論をTypeProfile型に上書きしたことで、 変数profileのnameとageそれぞれのプロパティが使用できるようになります。

    assertion3.png

    型アサーションは、TypeScriptコンパイラの型推論をプログラマが指定した型に上書きする強力な機能です。 しかしながら、TypeScriptの型推論を上書きすることはTypeScriptによる型安全を失うことになります。 次の例は、型アサーションの不適切な使用例です。

    関数onlyStringは引数に文字列型、もしくは数値型を受け取ります。 ブロック内で、引数で受け取ったvalueを型アサーションを使い文字列型に上書きし、変数strに格納します。 sliceメソッドはStringオブジェクトのプロトタイプメソッドです。文字列型のみ利用できます。

    function onlyString(value: string | number) {
    const str = value as string;
    console.log(str.slice(0, 2));
    };

    次のように数値型の引数12345を渡した場合、渡された数値は型アサーションにより文字列型に上書きされます。

    onlyString(12345);

    文字列型に指定された変数strに数値が格納されてしまいます。

    型付けは値そのものを書き換えることはありません。 関数onlyStringの変数strには数値が格納されています。 型はコンパイル時に削除されるため、 JavaScripコードの実行時はランタイムエラーが発生します。

    assertion4.png

    型アサーションは、TypeScriptコンパイラの型認識を切り替えるのみで 値に変更を加えることはありません。 不適切な型で型アサーションを指定した場合、バグ混入の原因になります。

    TypeScriptの型推論を上書きすることは、TypeScriptによる型安全を失うことになります。 このため、プログラマの意図する型に上書きする以外は、型アサーションの使用は極力避けるべきです。

    型ガード

    型ガードとは、変数の持つ値が特定の型であるかどうかを事前に調べることで、値が特定の型であるものとして扱えるようにする機能です。 型によって処理を分けたい場合に使います。 型の絞り込みを行うことでTypeScriptによる型の安全が保証されます。

    typeof演算子

    JavaScriptが実装するtypeof演算子を使うことで、 JavaScriptに定義されているデータ型のうち、プリミティブな値の型を調べることができます。

    次の関数stringNumberは、文字列型もしくは数値型の引数を受け取り、引数で受け取った文字列の最初の2文字を取得して返します。 Stringオブジェクトのsliceメソッドは数値を受け取れません。 このままでは数値が渡された場合、エラーが発生します。

    function stringNumber(value: string | number) {
    return value.slice(0, 2);
    };

    例えば、次のように関数stringNumberに型の絞り込みを実装します。

    function stringNumber(value: string | number) {
    if (typeof value === 'string') {
    return value.slice(0, 2);
    }
    else {
    return value + 10;
    }
    };

    typeof演算子は未評価のオペランドの型を表す文字列を返します。 比較演算子を使い、typeof value === 'string'であるかどうかをif文で調べます。 ===は厳密等価演算子と呼ばれます。暗黙の型変換を行いません。 左辺のtypeof valueの値の型名と右辺の'string'が同じ型で同じ値であれば真偽値のtrueを返します。 変数の値の型が'string'であれば、ブロック内で引数の値は文字列型として処理され、最初の2文字を取得し返します。 それ以外の場合、+10した値を返します。

    コンソールから確認してみましょう。関数stringNumberに渡した値の型が文字列型の場合、最初の2文字を取得した値が表示されます。 数値型の場合は+10された値が表示されます。

    console.log(stringNumber('hello'));
    
    console.log(stringNumber(10));

    typeof5.png

    情報

    2022年7月現在、ECMAScript®2022言語仕様ECMA-262では、 データ型は 真偽値型、数値型、Bigint型、文字列型、シンボル型、undefined、null、オブジェクトの8つの型が定義されています。

    情報

    オペランドとは、演算子が値を処理する時に演算子の対象となる値を指します。 例えば1+2の場合、1と2がオペランドです。

    instanceof演算子

    あるインスタンスが特定のクラスのインスタンスであるかどうかを判別する場合、instanceof演算子を使います。 JavaScriptが実装するinstanceof演算子は、インスタンスのプロトタイプが、 クラスのプロトタイプチェーンの中に存在するかどうかを検索し、存在すれば真偽値のtrueを返します。

    実際に試してみます。 次のようにTomとJohnの2つのクラスを宣言します。 クラスTomはnameメソッドを持ち、クラスJohnはnameとageの2つのメソッドを持ちます。

    class Tom {
    name() {
    console.log('Tomです。');
    }
    }
    
    class John {
    name() {
    console.log('Johnです。');
    }
    age(count: number) {
    console.log(`${count}才です。`);
    }
    }

    次の関数peopleは、引数にTomもしくはJohnのどちらかのクラスの型を受け取る関数です。 引数にnameとageの2つの値を含む場合、name、ageそれぞれのメソッドを実行します。 name、ageどちらか一方でも含まない場合はエラーとなります。

    function people(person: Tom | John) {
    person.name();
    person.age();
    };

    Tom型はageメソッドを持ちません。 関数peopleはコンパイルエラーが発生します。

    instanceof6.png

    ageを含む場合の処理を分けることで、Tom、Johnの2つのクラスの型を引数として 受け取れるようにします。 まず、クラスJohnをnew演算子でインスタンス化し変数johnに代入します。

    const john = new John();

    次に関数peopleに型の絞り込みを実装します。

    function people(person: Tom | John) {
    person.name();
    if (person instanceof John) {
    john.age(35);
    }
    };

    person instanceof Johnでは、instanceof演算子を使って、渡されたpersonインスタンスがJohn型のプロトタイプチェーンの中に存在するかどうかを検索しています。 存在すれば真偽値のtrueを返します。 John型のインスタンスであれば、ブロック内で引数personはJohn型のインスタンスとして処理されます。johnインスタンスが持つageメソッドに35を渡します。

    name、age2つの値を持つjohnインスタンスを関数peopleに渡します。 nameメソッドが実行され、 かつ、John型のインスタンスと判別されたことでageメソッドが実行されます。

    people(john);

    instanceof5.png

    クラスTomをnew演算子でインスタンス化し変数tomに代入します。

    const tom = new Tom();

    ageメソッドのないtomインスタンスを関数peopleに渡します。 John型のインスタンスと判別されずnameメソッドのみ実行されます。

    people(tom);

    instanceof4.png

    情報

    プロトタイプは、JavaScriptオブジェクトが機能を継承する仕組みです。 あるオブジェクトが他のオブジェクトの性質を継承し継承元のオブジェクトの性質を参照することで、 自身のものであるかのように振る舞うといったものです。
    プロトタイプチェーンは、オブジェクトが継承元のプロトタイプにさかのぼって プロパティやメソッドを参照することを指します。

    in演算子

    JavaScriptが実装するin演算子は、指定したオブジェクトに特定のプロパティが存在するかどうかを判別します。 存在する場合ば真偽値のtrueを返します。

    次のようにHuman型とAnimal型を宣言します。 Human型はnameとageの2つのプロパティを持ち、Animal型はnameとplaceの2つのプロパティを持ちます。

    interface Human {
    name: string;
    age: number;
    }
    
    interface Animal {
    name: string;
    place: string;
    }

    次の関数humanAnimalは、Human型もしくはAnimal型の引数valueを受け取ります。 引数で渡されたvalueのageを表示します。

    function humanAnimal(value: Human | Animal) {
    console.log(value.age);
    };

    Animal型はageプロパティを持たないため、今のままではコンパイルエラーが発生します。

    in1.png

    valueがageプロパティを含む時の処理と含まない時の処理を分ける場合、 次のように関数humanAnimalに型の絞り込みを実装します。

    function humanAnimal(value: Human | Animal) {
    if ('age' in value) {
    console.log(value.age);
    } else {
    console.log(value.place);
    }
    };

    'age' in valueでは、 渡された引数valueの中に、ageプロパティが存在するかどうかをin演算子を使って検索しています。 存在すれば真偽値のtrueを返します。 ブロック内でvalueがageを含む場合の処理が実行されます。

    関数humanAnimalにageを含み、Human型を満たす値を引数として渡します。 ageが含まれるため、valueが持つageプロパティの値が表示されます。

    humanAnimal({ name: "Tom", age: 22 });

    in4.png

    関数humanAnimalにageを含まないAnimal型を満たす値を引数として渡します。 ageを含まないため、valueが持つplaceプロパティの値が表示されます。

    humanAnimal({ name: "Rayray", place: '動物園' });

    in5.png

    ユーザー定義ガード

    今まで紹介した型ガードは、TypeScriptによる型の安全が保証されていました。 しかし、ユーザー定義ガードは、TypeScriptによる型の安全が保証されません。 本項では、ユーザー定義ガードの実装方法、 プログラマが負う責任範囲を説明していきます。

    ユーザー定義ガードは、受け取る引数が複雑で、 typeof演算子やinstanceof演算子にアクセスできないといった場合、 ユーザーが定義できる型ガードになります。 戻り値の型に引数名 is 型と指定します。引数名 is 型という形を型述語と言います。 型述語が書かれたユーザー定義ガードの戻り値は真偽値になり、 tureが返された場合は引数名が型であることを示します。

    次のように、ユーザー定義ガードである関数isStringOrNumberを定義します。

    function isStringOrNumber(value: unknown): value is string | number {
    return typeof value === "string" || typeof value === "number";
    }

    関数isStringOrNumberはunknown型の引数を受け取ります。 引数のデータ型が"string" もしくは"number"であればtrueを返します。 戻り値の型にvalue is string | numberと型述語を指定しています。 tureが返った場合、valueはstring | number型になります。 つまり、戻り値の型はstring | number型になります。

    実際にユーザー定義ガードを試してみます。 次のように関数sampleUnknownを定義します。

    function sampleUnknown(v: unknown) {
    if (isStringOrNumber(v)) {
    console.log(v.toString());
    }
    }

    関数sampleUnknownはunknown型の引数を受け取ります。 if文の条件で関数isStringOrNumberを呼び出して引数を渡しています。 if文の条件に 型述語を記述したユーザー定義ガードを指定することで、 型の絞り込みをしていることをTypeScriptコンパイラに示しています。 関数isStringOrNumberからtrueが返された場合、ブロック内では 関数isStringOrNumberの戻り値の型はstring | number型になります。 つまり、ブロック内では引数で渡されたvの型はstring | number型として処理されます

    userguard1.png

    次に型述語を指定しなかった場合にどのような挙動になるのかを見てみます。 次のように、関数isStringOrNumberの戻り値の型をbooleanに変更してみましょう。

    function isStringOrNumber(value: unknown): boolean {
    return typeof value === "string" || typeof value === "number";
    }

    userguard2.png

    型述語を指定しなかった場合、型の絞り込みが行われません。 引数で渡されたvの型はunknown型のままです。 型述語の指定は、TypeScriptコンパイラに型の絞り込みを行っていることを示すために必要になります。

    最後に、ユーザー定義ガードの実装を間違えた場合の挙動を見てみます。 関数isStringOrNumberのvalue === "string"をvalue === "boolean"に変更します。

    function isStringOrNumber(value: unknown): value is string | number {
    return typeof value === "boolean" || typeof value === "number";
    }

    実装に誤りがあるにも関わらず、コンパイルエラーは発生しません。 これは、関数内において型述語で書かれた通りの絞り込みを行っているかどうかについて、 TypeScriptは安全を保証しないためです。 このように、ユーザー定義ガードは誤った使用をすると型の安全を損なうことになります。 しかしながら、プログラマがカバーすべきユーザー定義ガードの責任範囲は明確です。 ユーザー定義ガードのロジックが正しいことを保証できればよい、ということになります。

    以前に紹介した、 TypeScriptの型安全を損なう機能としてany型とasがありました。 もしこの2つの機能のどちらかを使わざるを得ない場合は、 ユーザー定義ガードが使えないかどうかを優先して検討してください。 any型は使用を避けるべきことは何度も説明しました。 また、asは型推論を上書きすることになり、型推論より正しい型であることをプログラマが保証しなければなりません。 ユーザー定義ガードは、ロジックが正しいことを保証できればよいので、プログラマが負う責任の範囲が明確になります。

    型の互換性

    型の互換性とは、型が代入可能であることを表します。

    型の互換性に基づき、抽象度が高い型には抽象度が低い型を代入できます。 一方、抽象度が低い型に代入できる型は限定的になります。 具体例を見てみましょう。

    Human型はnameとageの2つのプロパティを持ちます。Person型はname、age、telの3プロパティを持ちます。

    type Human = {
    name: string,
    age: number
    }
    type Person = {
    name: string,
    age: number
    tel: number
    };

    次の変数obj1は、name、age、telの3つのプロパティを持ち、Person型に型注釈されています。 変数obj2は、name、ageの2つのプロパティを持ち、Human型に型注釈されています。

    const obj1: Person = {
    name:'Tom',
    age: 22,
    tel: 123456
    };
    
    const obj2: Human = {
    name:'Tom',
    age: 22,
    };

    次の例は部分型になります。 Person型の変数obj1をHuman型の変数obj3に代入します。

    const obj3: Human = obj1;

    Person型はHuman型が持つname、ageの2つのプロパティを持ちます。 部分的に構造が一致するためPerson型はHuman型の部分型になります。 Person型はHuman型のサブタイプ、Human型はPerson型のスーパータイプとして扱われます。 サブタイプはスーパータイプに代入可能です。 Person型はHuman型へ代入することができます。

    次の例は部分型になりません。 Human型の変数obj2をPerson型の変数obj4に代入します。

    const obj4: Person = obj2;

    Human型にはPerson型のオブジェクト型がもつtelプロパティが含まれず条件を満たしていません。 Human型はPerson型の部分型にはなりません。 Human型をPerson型に代入することはできません。

    bubun1.png

    上記以外にもTypeScriptに組み込まれている型の関係性があります。 例えば、以下のようなものです。

    • any型は全ての型のスーパータイプ
    • オブジェクト型は配列型のスーパータイプ
    • 配列型はタプル型のスーパータイプ
    • neverは全ての型のサブタイプ

    スーパータイプが期待される場合にサブタイプを代入することができます。 但し、any型はどのような値の型にも代入できます。

    情報

    2つのオブジェクト型を比較し部分型かどうかを決める仕組みは構造的部分型と言われます。 TypeScriptは構造的部分型という型システムを採用しています。

    Lesson 2 Chapter 12
    ジェネリクス

    関数やクラスが定義された時に型が決まっておらず、 呼び出すときに型を柔軟に指定できる機能を作る時にジェネリクスを使います。 本章ではジェネリクスを使った関数、クラス、Conditional Type、Mapped Typesを 具体例を挙げて説明していきます。

    関数

    ジェネリクスを使った関数とは、関数を定義する時に抽象的な型で型引数を定義し、関数を呼び出す時に具体的な型引数を渡すことで、型が決まる関数です。 関数を定義する時に型引数のエイリアスとして<T>を指定します。 エイリアスとは別名という意味です。 Tは慣例的に使われる型引数名です。

    実際に試してみます。 次の関数returnSomethingは、T型の型引数を受け取りT型のvalueを返す関数です。 型が確定していないため型引数にTを指定しています。

    function returnSomething<T>(value: T): T {
    return value;
    }

    次のように具体的な型引数を渡して関数を呼び出します。

    returnSomething<number>(22);

    関数名<number>といったように型引数に数値型を渡します。 型引数に数値型を渡したことで引数valueと戻り値の型Tが数値型に置き換わります。 引数valueには数値の22を渡しています。 valueはそのまま戻り値となり22を返します。 コンソールから出力を確認します。 数値型の戻り値22が表示されます。

    console.log(returnSomething<number>(22));

    generic1.png

    このようにジェネリクスを使うことで、関数を呼び出すときに 具体的な型引数を渡すことができます。

    次にジェネリクスを使うメリット紹介します。

    先ほど数値型を渡した関数returnSomethingを流用します。 次のように型引数に文字列型、引数に文字列'Hello'を渡します。

    returnSomething<string>('Hello');

    型引数に文字列型を渡したことで引数と戻り値の型Tが文字列型に置き換わります。 文字列型の引数valueには文字列の'Hello'を渡しています。 valueはそのまま戻り値となり'Hello'を返します。 コンソールから出力すると文字列型の戻り値'Hello'が表示されます。

    console.log(returnSomething<string>('Hello'));

    generic1.png

    このように、同じ処理の関数であれば一つの関数にまとめて共通化することができます。 共通化することでコード量を減らすといったことができます。

    情報

    型引数とは、型を定義するときにパラメータを持たせることができるというものです。 例えば関数を定義するときに関数名に続けて指定します。 関数名<T>というように<>で型を囲って指定します。

    複数の型引数を受け取る場合

    複数の型引数を受け取る場合、習慣的にT、Uなどの大文字のアルファベットを型引数名として使います。

    関数returnSomething2は2種類の型引数<T, U>を受け取り、U型のvaue2を返す関数です。

    function returnSomething2<T、U>(value1: T, value2: U): U {
    return value2;
    };

    具体的な型引数を渡して関数を呼び出します。

    returnSomething2<string, number>('Hello', 20);

    returnSomething2<string, number>といったように型引数に文字列型と数値型を渡します。 value1の型Tは文字列型に、value2の型Uと戻り値の型Uは数値型に置き換わります。 value1には文字列の'Hello'、value2には数値の20を渡しています。 数値型のvalue2はそのまま戻り値となり20を返します。

    コンソールから出力を確認します。数値型の戻り値20が表示されます。

    console.log(returnSomething2<string, number>('Hello', 20));

    generic5.png

    extendsによる制約

    ジェネリクスの型Tを特定の型に制約することで型の安全を高めることができます。 extendsキーワードを<型引数 extends 型>というように指定します。 型引数は型の条件を満たす部分型であるといった制約になります。

    実際に試してみます。 Mike型はstring型のnameとnumber型のageの2つプロパティを持ちます。

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

    次の関数peopleは、Mike型に制約された型引数Tを受け取ります。 型引数がMike型の部分型であれば、引数の型TもMike型の部分型に置き換わります。 引数valueがMike型を満たす条件のプロパティを持つ場合、 valueに含まれるnameとageの2つの値を返します。

    function people<T extends Mike>(value: T) {
    return {
    name: value.name,
    age: value.age,
    }
    };

    extendsによる制約ができているか確認します。 次のように関数peopleに、 nameプロパティに'MIKE'、ageプロパティに22を持つ Mike型のプロパティを満たすオブジェクトを引数として渡します。 関数peopleからの戻り値を変数mike1に格納します。

    const mike1 = people({ name: 'MIKE', age: 22 });
    
    console.log(mike1);

    generic11.png

    Mike型を満たす型引数を渡せているためエラーは発生しません。 コンソールから確認すると渡した引数が表示されることが確認できます。

    次に、Mike型を満たさない場合の制約ができているか確認します。 nameプロパティのみの Mike型を満たさない引数を渡します。

    const mike2 = people({ name: 'mike' });

    generic33.png

    Mike型を満たすにはnameとageの2つのプロパティを持つオブジェクトを引数として渡す必要があります。 引数の型がMike型の部分型ではないためコンパイルエラーが発生します。

    制約により型引数が型の部分型であることが保証されるため、型の安全が高まっていることがわかります。

    情報

    部分型とは構造的に型が一致するのであればサブタイプとして扱うというTypeScriptの機能です。

    クラス

    クラスの場合も、具体的な型引数をインスタンス時に渡したい場合、クラス宣言時の型引数にジェネリクスを使うことができます。 型引数にはエイリアスとして<T>を使います。

    次のクラスReturnSomethingはT型の型引数を受け取り、コンストラクタ関数で初期化し、getDataメソッドからT型のvalueを返します。

    class ReturnSomething<T> {
    value: T;
    constructor(value: T) {
    this.value = value;
    }
    
    getData(): T {
    return this.value;
    }
    }

    次のようにクラスReturnSomethingの型引数Tに文字列型を渡し、インスタンス化したオブジェクトを 変数strに格納します。

    const str = new ReturnSomething<string>('Hello');

    型引数に文字列型を渡したことでvalueの型T、コンストラクタの引数の型T、getDataメソッドの戻り値の型Tが文字列型に置き換わります。 文字列型の引数valueには文字列の'Hello'を渡しています。 ReturnSomethingインスタンスのgetDataメソッドは受け取った文字列型のvalueを返します。

    変数strが持つgetDataメソッドを呼び出しコンソールから出力を確認します。 文字列型の戻り値'Hello'が表示されます。

    console.log(str.getData());

    generic8.png

    次に、クラスreturnSomethingが再利用できることを確認します。 クラスreturnSomethingの型引数Tに数値型を渡し、インスタンス化したオブジェクトを変数numに格納します。

    const num = new ReturnSomething<number>(20);

    型引数に数値型を渡したことでvalueの型T、コンストラクタの引数の型T、getDataメソッドの戻り値の型Tが数値型に置き換わります。 数値型の引数valueには数値の20を渡しています。

    次のように変数numが持つgetDataメソッドを呼び出しコンソールから出力します。 数値型の戻り値20が表示されます。

    console.log(num.getData());

    generic55.png

    このように、ジェネリクスを使うことで型を柔軟に指定することができます。 同じデータ構造と処理を持つクラスであれば一つのクラスにまとめて共通化することができます。

    extendsによる制約

    クラスについてもジェネリクスの型Tを特定の型に制約することで、型の安全を高めることができます。 extendsキーワードを<型引数 extends 型>というように指定します。 型引数は型の条件を満たす部分型であるといった制約になります。

    実際に試してみます。 Greet型は文字列型のgreetingプロパティを持ちます。

    interface Greet {
    greeting: string;
    }

    次のクラスReturnSomethingの型引数TはGreet型に制約された型引数Tを受け取ります。 valueの型T、コンストラクタの引数の型T、getDataメソッドの戻り値の型TがGreet型の部分型に置き換わります。 getDataメソッドは受け取った文字列型のvalueを返します。

    class ReturnSomething<T extends Greet> {
    value: T;
    constructor(value: T) {
    this.value = value;
    }
    getData(): T {
    return this.value;
    }
    }

    extendsによる制約ができているか確認します。 次のようにクラスReturnSomethingにGreet型の条件を満たす引数を渡しインスタンス化し、変数greet1に格納します。 引数には{greeting: 'Hello'}オブジェクトを渡します。 Greet型が持つgreetingプロパティの型は文字列型です。文字列型を満たす型引数を渡せているためエラーは発生しません。

    const greet1 = new ReturnSomething({ greeting: 'Hello' });

    次のようにコンソールから確認すると渡した引数が表示されることが確認できます。

    console.log(greet1.getData());

    generic12.png

    次に、Greet型を満たさない場合の制約ができているか確認します。 Greet型を満たさない{greeting: 22}オブジェクトを引数に渡します。

    const greet2 = new ReturnSomething({ greeting: 22 });

    generic13.png

    Greet型を満たすにはgreetingtプロパティに文字列型を渡す必要があります。 Greet型の部分型ではない引数を渡したためコンパイルエラーが発生します。

    制約により型引数が部分型であることが保証されるため、型の安全が高まっていることがわかります。

    Conditional Types

    Conditional Typesは、三項演算子のような書き方で、型定義による条件分岐ができる型です。 <T extends U ? A : B>のように記述し、TがUを満たす場合はA、満たさない場合はBといった分岐になります。

    次の例では、型引数Tが文字列型を満たした場合はture、それ以外の場合はfalseを返すIsString型を定義しています。

    type IsString<T> = T extends string ? true : false;

    まず、tureを返す場合を見てみます。 次のようにIsString型の引数に文字列の'a'を渡します。返された新しいオブジェクト型をStrと定義します。

    type Str = IsString<'a'>;

    conditional1.png

    'a'は文字列型を満たす引数です。Str型にはtrueが返ります。

    次にfalseを返す場合を見てみます。IsString型の引数に数値の1を渡します。

    type Str = IsString<1>;

    conditional2.png

    1は文字列型を満たさない引数です。Str型にはfalseが返ります。

    Conditional Typesを使うことで、条件を満たした場合と満たさなかった場合で、分岐ができていることが確認できました。 では、どのような場合にConditional Typesを使うのでしょうか。

    Conditional Typesは、例えば特定の型を取り出すExtract、nullとundefinedを除外するNonNullablといったユーティリティ型に使われています。 ユーティリティ型についてはユーティリティ型の章で説明します。 例えばExtract型の型定義は次の通りです。

    type Extract<T, U> = T extends U ? T : never;

    TがUを満たす場合はT、それ以外の場合はneverを返す型です。

    次のようにExtractを参考に、ExtractHuman型を定義して実際に試してみます。 ExtractHuman型は、型引数Tが{ walk : 'slow' }の部分型である場合は{ walk : 'slow' }を、 それ以外の場合はneverを返します。

    type ExtractHuman<T> = T extends { walk: 'slow' } ? T : never;

    Human型、Animal型、Fish型を定義ます。 Human型、Animal型、Fish型の3つの型を持つユニオン型のCreature型を定義します。

    type Human = { walk : 'slow' };
    type Animal = { run : 'fast' };
    type Fish = { swim : 'fast' };
    
    type Creature = Human | Animal | Fish;

    次に、ExtractHuman型の型引数にCreature型を渡し、取り出したオブジェクト型をPersonと定義します。

    type Person = ExtractHuman<Creature>;

    ExtractHuman型には、Human型、Animal型、Fish型の順で型引数が渡されます。 ExtractHuman型のT extends { walk: 'slow' }により、{ walk : 'slow' }を満たす型引数であるかをチェックしています。 満たす場合は{ walk : 'slow' }を返し、満たさない場合はneverを返します。 never型は値を持たない型を表します。このためnever型はユニオン型内で自動で取り除かれます。 Person型の型推論を見ると、{ walk : 'slow' }のみプロパティを持つ型と確認できます。

    conditional-ext2.png

    部分的な型抽出

    Conditional Types構文の中では、inferを使うことで部分的な型抽出ができます。 inferを使った参考例としてユーティリティ型のReturnTypeが挙げられます。 ReturnTypeは関数の戻り値の型を返す型です。 型定義を見てみましょう。

    type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

    =以降のT extends (...args: any) => infer R ? R : any;では、 引数を受け取る関数型Tの戻り値の型をRと推測し、RがあればRを取り出すといった処理をしています。 つまり関数に戻り値の型がある場合、戻り値の型を取り出し返しています。

    inferは、ある型の中に特定の型が含まれ、その特定の型を取り出したい場合に使います。

    実際にReturnTypeを試してみます。 次の関数add1は引数に数値型のa、数値型のbを受け取ります。戻り値としてaとbを加算した数値を返します。

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

    次のようにReturnTypeの型引数に関数型のadd1を指定し、 返された新たなオブジェクト型をReturnTypeFormと定義します。

    type ReturnTypeForm = ReturnType<typeof add1>;

    数値型の引数を受け取る関数add1の戻り値の型は数値型です。inferにより数値型を取り出します。 次の型推論を見るとReturnTypeForm型は数値型であると確認できます。

    returntype.png

    Mapped Types

    Mapped Typesは、あるオブジェクトのプロパティ名を利用して新しい型を定義する時に使います。

    [P in K]: T

    [P in K]: Tを利用することでKに含まれるPをプロパティキーとして、Tをプロパティの値に指定したオブジェクト型を作ります。

    次の例では、ユニオン型のAnimal型をKに指定し、[P in K]: Tを利用して返された新たなオブジェクト型をAnimalNumbersと定義した使用例です。

    ユニオン型のAnimal型は'dog''cat''bird'の3つのプロパティを持ちます。

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

    次のAnimalNumbers型の[P in Animal]: Number;では、 Animal型に含まれる'dog''cat''bird'の3つの要素をプロパティ名とし、 数値型をプロパティの値に指定した新たなオブジェクト型を返しています。

    type AnimalNumbers = {
    [P in Animal]: Number;
    };

    型推論を見るとAnimalNumbers型は、'dog''cat''bird'の3つのプロパティ名を持ち、プロパティの値として数値型を持つオブジェクト型と確認できます。

    mapped2.png

    [P in keyof T]: T[P]

    [P in keyof T]: T[P]を利用することでTに含まれるPをプロパティキーとして、T[P]をプロパティの値に指定したオブジェクト型を作ります。

    [P in keyof T]: T[P]を使った参考例としてユーティリティ型のPartialが上げられます。 Partial型は、型引数として受け取る全てのプロパティをオプショナルのプロパティに変換する型です。 型定義を見てみましょう。

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

    in演算子とkeyof演算子を使い型Tのプロパティ名Pを一つずつ抽出しています。Pに対してオプションパラメータを設定してオプショナルのプロパティに変換しています。 変換したプロパティ名をキーとして、T[P]をプロパティの値に指定して新たなオブジェクト型を返します。

    次の例は、Partial型を流用したMyPartial型を定義し、[P in keyof T]?: T[P]を利用した使用例です。 まず、nameとageの2つのプロパティを持つProfile型を定義します。

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

    次のようにPartialのTにProfile型を指定し、返された新たなオブジェクト型をMyPartialと定義します。

    type MyPartial = {
    [P in keyof Profile]?: Profile[P];
    };

    [P in keyof Profile]?: Profile[P];では、 まず、profile型のnameとageをプロパティキーとして取り出し、オプションパラメータを指定しています。 次に、Profile型からプロパティキーに対する値を取り出し、プロパティ値として指定しています。 つまり、nameの値はstring、ageの値はnumberが指定され、 すべてのプロパティがオプショナルの値に変換された新たなオブジェクト型を返しています。

    MyPartial型の型推論を確認します。

    mapped3.png

    Profile型が持つnameとageの2つのプロパティがオプショナルの値に変換されたオブジェクト型を持つことが確認できます。

    Lesson 2 Chapter 13
    ユーティリティ型

    ユーティリティ型を使うことで、ベースとなる型を変換して新しい型を定義できます。 2022年7月現在、本章で取り上げるユーティリティ型はすべてTypeScriptのビルトインパッケージとして定義されています。 型定義のソースコードを見ることができます。 VSCodeを使っている場合の型定義の見方は次の通りです。 例えば、ユーティリティ型のPartialのソースコードを見たい時は、型名のPartial上で右クリックします。

    type PartialType = Partial<Profile>;

    表示されるメニュー上段の定義へ移動をクリックすると 型のソースコードを見ることができます。

    teigi1.png

    teigi2.png

    Partial<T>

    Partial<T>は型引数Tの全てのプロパティをオプショナルのプロパティに変換し新たな型を返す型です。

    Partialの型定義を見てみます。

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

    in演算子とkeyof演算子を使い、Tのプロパティ名Pを一つずつ抽出しています。Pに対してオプションパラメータを設定してオプショナルのプロパティに変換しています。

    実際に試してみます。 次のProfile型はname、ageの2つのプロパティを持ちます。全てのプロパティは必須のプロパティです。

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

    次のようにPartialの型引数にProfile型を渡します。 Profile型を変換した新たな型をPartialTypeと定義します。

    type PartialType = Partial<Profile>;

    PartialType型はProfile型が持つ全てのプロパティをオプショナルの プロパティに変換したオブジェクト型であると確認できます。

    utility-partial3.png

    Required<T>

    Required<T>は型引数Tの全てのプロパティを必須のプロパティに変換し新たな型を返す型です。

    Requiredの型定義を見てみます。

    type Required<T> = {
    [P in keyof T]-?: T[P];
    };

    in演算子とkeyof演算子を使い、Tのプロパティ名Pを一つずつ抽出しています。Pのオプションパラメーターを取り除いています。

    実際に試してみます。 次のProfile型はname、ageの2つのプロパティを持ちます。全てのプロパティはオプショナルのプロパティです。

    type Profile = {
    name?: string;
    age?: number;
    };

    次のようにRequiredの型引数にProfile型を渡します。 Profile型を変換した新たな型をRequiredTypeと定義します。

    type RequiredType = Required<Profile>;

    RequiredType型はProfile型が持つ全てのプロパティを必須のプロパティに変換したオブジェクト型であると確認できます。

    utility-partial2.png

    Readonly<T>

    Readonly<T>は型引数Tの全てのプロパティを読み取り専用のプロパティに変換し新たな型を返す型です。

    Readonlyの型定義を見てみます。

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

    in演算子とkeyof演算子を使い、Tのプロパティ名Pを一つずつ抽出しています。Pに対してreadonly修飾子を与えて読み取り専用のプロパティに変換しています。

    実際に試してみます。 次の変数Profile型はname、ageという2種類のプロパティを持ちます。

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

    次のようにReadonlyの型引数にProfile型を渡します。 Profile型を変換した新たな型をReadonlyTypeと定義します。

    type ReadonlyType = Readonly<Profile>

    ReadonlyType型はProfile型が持つ全てのプロパティを 読み取り専用に変換したオブジェクト型であると確認できます。

    utility-readonly1.png

    Recoad<K, T>

    Record<K, T>は、プロパティのキーがK、プロパティの値がTである新たなオブジェクト型を返します。

    Recoadの型定義を見てみます。

    type Record<K extends keyof any, T> = {
    [P in K]: T;
    };

    [P in K]: T により、Kに含まれるPをプロパティキーとし一つずつ取り出し、取り出したPに対してプロパティ値としてTを指定して返しています。

    実際に試してみます。 次のようにnameとageの2つのプロパティを持つPerson型を宣言します。

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

    次に4つのプロパティを持つ変数memberを宣言します。各プロパティに対してPerson型を型注釈しています。

    const member:{
    one: Person,
    two: Person,
    three: Person,
    four: Person,
    } = {
    one: { name: 'Tom', age: 22 },
    two: { name: 'John', age: 25 },
    three: { name: 'Mike', age: 28 },
    four: { name: 'Kevin', age: 35 },
    };

    変数memberは、オブジェクトの数だけプロパティ名に型注釈を繰り返す必要があります。

    このような場合、プロパティ名をユニオン型を使った別の型として定義します。 次のMember型は変数memberのプロパティ名を別の型として定義した型になります。

    type Member = 'one' | 'two' | 'three' | 'four';

    Recoad型のK、Tに型を当てはめます。 ユニオン型のMember型をKに、Person型をTに渡します。 冗長だったオブジェクト型をRecord型を使うことで簡素に定義できます。

    const member: Record<Member, Person> ={
    one: { name: 'Tom', age: 22 },
    two: { name: 'John', age: 25 },
    three: { name: 'Mike', age: 28 },
    four: { name: 'Kevin', age: 35 },
    };

    Pick<T, K>

    Pick<T, K>は、ある型から使いたいプロパティを抽出した新たな型を返す型です。

    Pickの型定義を見てみます。

    type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
    };

    K extends keyof Tでは、KはTのオブジェクト型を含んでいることを示しています。 [P in K]: T[P]では、まず、Kが持つプロパティのプロパティ名をPとして一つずつ抽出しています。 次にPをプロパティキーとしてTから取り出した値をプロパティ値としています。 つまり、Pをプロパティ名、KとTが共通して持つPをプロパティキーとしてPから取り出し値をプロパティ値とした新たな オブジェクト型を返しています。

    実際に試してみます。次のようにname、height、weightの3つのプロパティを持つProfile型を定義します。

    type Profile = {
    name: string;
    height: number;
    weight: number;
    };

    Profile型からname、weightを抽出した新たな型を定義したい場合、 Profile型をPickのTに、'name' | 'weight'を含んだユニオン型をPickのKに渡します。 取り出した新たな型をSimpleProfileと定義します。

    type SimpleProfile = Pick<Profile, 'name' | 'weight'>;

    SimpleProfile型はnameとweightの2つのプロパティを持つオブジェクト型であると確認できます。

    utility-pick2.png

    Omit<T, K>

    Omit<T, K>は、ある型から使わない一部のプロパティを除いた型を新たな型として返す型です。

    Omitの型定義を見てみます。

    type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

    Exclude<keyof T, K>によりTとKに共通するプロパティを取り除きます。 残ったプロパティがTに含まれる場合、Tに含まれるプロパティをオブジェクト型として返します。

    実際に試してみます。 次のProfile型はname、height、weightの3つのプロパティを持っています。

    type Profile = {
    name: string;
    height: number;
    weight: number;
    };

    Profile型からheightを除いた新たな型を定義したい場合、 Profile型をOmitのTに、除きたいプロパティ'height'をOmitのKに渡します。 取り除いた新たなオブジェクト型をSimpleProfileと定義します。

    type SmallProfile = Omit<Profile, 'height'>;

    SimpleProfile型はnameとweightの2つのプロパティを持つオブジェクト型であると確認できます。

    generic5.png

    Exclude<T, U>

    Exclude<T, U>は複数の型から共通したプロパティを取り除いた型を新たな型として返す型です。

    Excludeの型定義を見てみます。

    type Exclude<T, U> = T extends U ? never : T;

    TがUのオブジェクト型を含む場合はnever、そうでない場合はTを返します。 つまり、共通するプロパティを除いています。 Conditional Typeが使われています。

    実際に試してみます。 次のTypeA型とTypeB型はそれぞれnameプロパティを持っています。

    interface TypeA {
    name: string;
    age: number;
    }
    interface TypeB {
    name: string;
    address: string;
    }

    TypeA型から、TypeA型とTypeB型に共通するnameプロパティを除いた新たな型を定義したい場合、 TypeA型をExcludeのTに、TypeB型をExcludeのUに渡します。 取り出した新たなオブジェクト型をExcludedTypeと定義します。

    type ExcludedType = Exclude<keyof TypeA, keyof TypeB>;

    ExcludedType型はageプロパティを持つオブジェクト型であると確認できます。

    utility-exclude.png

    情報

    Conditional Typesは、三項演算子のような書き方で、型定義による条件分岐ができる型です。

    Extract<T, U>

    ExtractはExcludeとは反対で、複数の型から共通したプロパティのみを残した型を新たな型として返す型です。

    Extractの型定義を見てみます。

    type Extract<T, U> = T extends U ? T : never;

    TがUのオブジェクト型を含む場合はT、そうでない場合はneverを返します。 つまり、共通しないプロパティを除いています。 Conditional Typeが使われています。

    実際に試してみます。 次のTypeA型とTypeB型はそれぞれnameプロパティを持っています。

    interface TypeA {
    name: string;
    age: number;
    }
    interface TypeB {
    name: string;
    address: string;
    }

    TypeA型から、TypeA型とTypeB型に共通するnameプロパティを残した新たな型を定義したい場合、 TypeA型をExtractのTに、TypeB型をExtractのUに渡します。 取り出した新たなオブジェクト型をExtractedTypeと定義します。

    type ExtractedType = Extract<keyof TypeA, keyof TypeB>;

    ExtractedType型はnameプロパティを持つオブジェクト型であると確認できます。

    utility-extract.png

    NonNullable<T>

    NonNullable<T>はnull,undefindを除いた型を新たな型として返す型です。

    NonNullableの型定義を見てみます。

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

    Tがnullもしくはundefinedを含む場合はnever、そうでない場合はTを返します。 つまり、nullとundefinedを除いています。 Conditional Typeが使われています。

    実際に試してみます。 次のユニオン型のNullableTypes型は、string | number | null | undefinedの4つの型を持ちます。

    type NullableTypes = string | number | null | undefined;

    NullableTypes型から、nullもしくはundefinedを取り除き新たな型を定義したい場合、 NullableTypes型をNonNullableのTに渡します。 取り出した新たなオブジェクト型をNonNullableFromと定義します。

    type NonNullableFrom = NonNullable<NullableTypes>;

    NonNullableFrom型はstringとnumberを持つ型であると確認できます。

    utility-nonnullable2.png

    Parameters<T>

    Parameters<T>は関数型の引数のパラメータからタプル型を作り新たな型として返す型です。

    Parametersの型定義を見てみます。

    type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

    =以降について、 T extends (...args: infer P) => any ? P : never;では、 関数型Tが引数Pを受け取り戻り値がある場合は引数のPを、 そうでない場合はneverを返します。 つまり、引数の型を返します。

    実際に試してみます。 次のように文字列型のnameと数値型のageの2つの引数を受け取り、 受け取った引数nameとageをコンソールからログとして出力する関数式profileを定義します。

    const profile = (name: string, age: number) => {
    console.log({ name, age });
    };

    Parametersを使って関数型profileの引数のパラメータを取得します。 ParametersのTに関数型profileを渡します。 新たに取得したオブジェクト型をReturnParametersと定義します。

    type ReturnParameters = Parameters<typeof profile>;

    ReturnParameters型は文字列型のnameと数値型のageの2つプロパティを持つタプル型であると確認できます。

    utility-parameters3.png

    情報

    inferはConditional Types構文の中で部分的な型抽出をしたい時に使うキーワードです。

    ConstructorParameters<T>

    ConstructorParameters<T>は、コンストラクタ関数の引数のパラメータをタプル型、もしくは配列型で抽出した型を新たな型として返す型です。

    ConstructorParametersの型定義を見てみます。

    type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

    =以降について、new (...args: infer P)は複数の引数を受け取るコンストラクタ関数を示します。 => any ? P : never;ではコンストラクタ関数Tの戻り値の型があれば引数Pを返し、なければneverを返しています。 つまり、コンストラクタ関数の引数の型を返します。

    実際に試してみます。 次のクラスPersonはnameとageの2つのフィールドを持つクラスです。

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

    ConstructorParametersを使ってコンストラクタ関数の引数のパラメータを取得します。 次のようにConstructorParametersの型引数にクラスPersonの型を渡します。 新たに取得したオブジェクト型をConstructorTypeと定義します。

    type ConstructorType = ConstructorParameters<typeof Person>;

    ConstructorType型は文字列型のnameと数値型のageの2つプロパティを持つタプル型であると確認できます。

    utility-parameters2.png

    ReturnType<T>

    ReturnType<T>は、関数の戻り値の型を受け取り新たな型として返す型です。

    ReturnTypeの型定義を見てみます。

    type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

    =以降について、T extends (...args: any) => infer R ? R : any;では 引数を受け取る関数型Tに戻り値の型RがあればRを返します。 つまり、戻り値の型を返します。

    実際に試してみます。 次の関数addは数値型のaと数値型のbの2つの引数を受け取りaとbを加算した数値を返します。

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

    関数addの戻り値の型を取り出します。 ReturnTypeの型引数に関数addの型を渡します。 新たに取得したオブジェクト型をReturnTypeAddと定義します。

    type ReturnTypeAdd = ReturnType<typeof add>;

    ReturnTypeAdd型は数値型を持つオブジェクト型であると確認できます。

    utility-returntype1.png

    InstanceType<T>

    InstanceType<T>は、コンストラクタ関数の戻り値の型で構成される型を新たな型として返す型です。

    InstanceTypeの型定義を見てみます。

    type InstanceType<T extends abstract new (...args: any) =>any>
    = T extends abstract new (...args: any) => infer R ? R : any;

    =以降について、new (...args: any)は複数の引数を受け取るコンストラクタ関数を示します。 => infer R ? R : any;ではコンストラクタ関数Tの戻り値の型があれば、戻り値の型を返しています。 この場合の戻り値の型はインスタンスの型そのものを表します。

    実際に試してみます。 次のクラスPersonはnameとageの2つのフィールドを持つクラスです。

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

    InstanceTypeを使ってPersonインスタンスの型を取得します。 InstanceTypeのTにクラスPersonの型を渡します。 新たに取得したオブジェクト型をPersonInstanceTypeと定義します。

    type PersonInstanceType = InstanceType<typeof Person>;

    PersonInstanceType型はPerson型を持っていると確認できます。

    utility-infer.png

    ThisParameterType<T>

    ThisParameterType<T>はthisのパラメータの型を取り出し、新たな型として返す型です。

    ThisParameterTypeの型定義を見てみます。

    type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;

    =以降について、(this: infer U, ...args: any[])では、引数に受け取るthisの型をUとして取り出しています。 any ? U : unknown;では戻り値があればthisの型Uを返しています。 つまり、引数でthisを受け取った場合、thisのパラメータの型を返します。

    実際に試してみます。 次の関数returnThisParameterTypeはthis、ageの2つの引数を受け取る関数です。 thisは{ name: string }をパラメータとして受け取ります。

    function returnThisParameterType(this: { name: string }, age: number): string {
    return this.name;
    };

    ThisParameterTypeを使って関数returnThisParameterTypeの引数thisの型を取得します。 次のようにThisParameterTypeの型引数に関数returnThisParameterTypeの型を渡します。 新たに取得したオブジェクト型をreturnThisParameterTypeFormと定義します。

    type returnThisParameterTypeForm = ThisParameterType<typeof returnThisParameterType>;

    returnThisParameterTypeForm型はthisのパラメータの型と同じオブジェクト型を持っていると確認できます。

    utility-thisreturntype3.png

    OmitThisParameter<T>

    OmitThisParameter<T>は受け取った引数からthisのパラメーターを取り除いたパラメータの型を、新たな型として返す型です。

    OmitThisParameterの型定義を見てみます。

    type OmitThisParameter<T> = unknown extends ThisParameterType<T> ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T;

    =以降について、unknown extends ThisParameterType<T>では、 unknown型はThisParameterTypeをextendsできませんのでthisの型を除きます。 (...args: infer A) => infer Rではthis以外の引数の型Aがあり戻り値Rがある場合、戻り値Rの型を取り出しています。 つまり、引数で受け取ったthis以外のパラメータの型を返します。

    実際に試してみます。 次の関数returnOmitThisParameterはthisとageの2つの引数を受け取る関数です。

    function returnOmitThisParameter(this: { name: string }, age: number): number {
    return age;
    };

    OmitThisParameterを使って関数returnOmitThisParameterのthis以外の引数の型を取得します。 次のようにOmitThisParameterの型引数に関数returnOmitThisParameterの型を渡します。 新たに取得したオブジェクト型を returnOmitThisParameterFormと定義します。

    type returnOmitThisParameterForm = OmitThisParameter<typeof returnOmitThisParameter>;

    returnOmitThisParameterForm型は受け取った引数からthisのパラメーターを取り除いたパラメータの型と同じオブジェクト型を持っていると確認できます。

    omitthis.png

    ThisType

    ThisTypeは指定した型でthisの型を置き換えることができる型です。

    ThisTypeの型定義を見てみます。

    interface ThisType<T>{ }

    ThisType<T>でthisは型引数Tに置き換えられます。

    実際に試してみます。 次のようにnameプロパティを持つ型Aと、helloメソッドを持つ型B、ageプロパティを持つ型Cを宣言します。

    interface A {
    name: string;
    }
    interface B {
    hello(): void;
    }
    interface C {
    age: string;
    }

    次のようにThisTypeの型引数に型Aを指定します。 B & ThisType<A>では型Bのthisの型をThisTypeで指定した型Aのthisの型に置き換えています。

    const obj: B & ThisType<A> = {
    hello() {
    console.log(`Hello, ${this.name}`);
    console.log(`Hello, ${this.age}`);
    },
    };

    ageプロパティは型Aに存在しないためコンパイルエラーが発生します。

    utility-thistype.png

    次に、ThisTypeの型引数に型Cを指定します。 B & ThisType<C>では型Bのthisの型をThisTypeで指定した型Cのthisの型に置き換えています。

    const obj: B & ThisType<C> = {
    hello() {
    console.log(`Hello, ${this.name}`);
    console.log(`Hello, ${this.age}`);
    },
    };

    nameプロパティは型Cに存在しないためコンパイルエラーが発生します。

    utility-thistype2.png

    thisの型が置き換わっていることを確認できます。

    Lesson 2 Chapter 14
    抽象クラス

    機能の追加によってプログラムの修正が必要になった場合、 追加処理の内容が既に実装しているクラスの処理内容と似ていることがあります。 似た処理の部分についてロジックを共通化し一つのクラスにまとめることができれば、 共通化したクラスを流用することで追加部分のみのコードの記述で修正が済みます。 ロジックを共通化して一つのクラスにまとめたい場合、抽象クラスを使うことができます。

    抽象クラスの特徴はいくつかあります。例えば以下の4つです。

    • 抽象クラス自身はインスタンス化できない。
    • 抽象クラスのメソッドは具体的な実装を持たない。
    • メソッドに具体的な実装を持たせるためには抽象クラスを継承したサブクラスを作る必要がある。
    • メソッドにはサブクラスで具体的な処理を実装する。

    この特徴に沿って本章は説明をしていきます。

    抽象クラス

    抽象クラスはabstract修飾子を使って宣言します。 クラス名の前にabstract修飾子を追加します。 abstract修飾子はメソッドにも追加できます。 実際に試してみます。 次のようにabstract修飾子を追加したクラスProfileを宣言します。

    abstract class Profile {
    name: string;
    
    constructor(name: string) {
    this.name = name;
    }
    
    abstract showDebug (): string;
    }

    クラスProfileは、 nameフィールド、文字列型を返すshowDebugメソッドを持ちます。 メソッドをサブクラスに引き継がせたいのでabstract修飾子を追加しています。 具体的な処理はここでは実装しません。

    まず、自身がインスタンス化できないことを確認します。

    abstract.png

    抽象クラスはインスタンス化できずコンパイルエラーが発生します。

    次に、abstract修飾子を追加した抽象クラスのメソッドに具体的な処理を実装します。

    abstract4.png

    実装しようとするとコンパイルエラーが発生します。 abstract修飾子を追加したメソッドには、具体的な処理を実装することはできません。 具体的な処理は、抽象クラスを継承したサブクラスを作り、 サブクラスのメソッドに実装します。

    次のようにクラスProfileを継承したクラスProfileWithAddressを宣言します。

    class ProfileWithAddress extends Profile {
    address: string;
    
    constructor(name: string, address: string) {
    super(name);
    this.address = address;
    }
    
    showDebug(): string {
    return `${this.name}は${this.address}に住んでいます`;
    }
    }

    クラスProfileWithAddressは、extendsキーワードを使ってクラスProfileを継承したクラスです。 クラスProfileWithAddressは、コンストラクタ内でsuperを使い親クラスのコンストラクタを呼び出しています。 nameとaddressの2つのフィールドを持ちます。

    showDebugメソッドに具体的な処理を記述します。 `${this.name}は${this.address}に住んでいます` と返す処理を実装します。

    abstract5.png

    コンパイルエラーは発生せず、 抽象クラスを継承したサブクラスのメソッドに具体的な処理を実装できることが確認できます。

    クラスProfileWithAddressをインスタンス化してメソッドが実行できるか確認してみます。 次のようにクラスProfileWithAddressに'Tom''東京都'の2つの引数を渡しインスタンス化します。 インスタンス化したオブジェクトを 変数tom1に代入します。

    const tom1 = new ProfileWithAddress('Tom', '東京都');

    変数tom1に格納されたProfileWithAddressインスタンスのshowDebugメソッドを実行し、 コンソールからログの出力を確認します。

    console.log(tom1.showDebug());

    abstract3.png

    2つの引数を受け取り、実装した内容が出力されていることが確認できます。

    抽象クラスを継承したサブクラスで、メソッドに具体的な処理を実装できることが確認できました。

    情報

    フィールドは、クラス内で定義された変数のことで、メンバー変数とも言います。

    abstractを追加したメソッドがサブクラスで未実装の場合

    showDebugメソッドにはabstract修飾子が追加されています。 クラスProfileを継承したサブクラスはshowDebugメソッドの実装が必須となります。 showDebugメソッド未実装時の挙動を確認してみます。

    abstract2.png

    コンパイルエラーが発生します。 これにより、メソッドが未実装の状態で据え置かれることを防ぐことができます。

    Lesson 2 Chapter 15
    TypeScriptを使用したフレームワーク

    フレームワークとは、 Webアプリケーションやシステムを開発する上で、土台となる必要な機能をパッケージ化したものです。 フレームワークを使うことで初期設定やよく使う機能を一から実装する労力が軽減され、 開発効率を向上させることができます。 JavaScriptの場合、React、Vue.jsといったフレームワークが代表例として挙げられます。

    本章では、TypeScriptを使って利用するフレームワークとして、 データベース操作で利用するTypeORM、 サーバサイドでJavaScripコードを実行するNode.js、 Node.jsのフレームワークであるNest.jsを紹介します。

    TypeORM

    TypeORMとは、Typescriptを用いてデータベースを操作するときに用いられるORMです。

    ORMとは、オブジェクトとリレーショナルデータベースのレコードを対応付けることで、 レコードをオブジェクトとして扱うといった仕組みです。 レコードはエンティティのクラスと紐づけられます。 データの追加、更新、削除といった操作はオブジェクトに変更を加えることでレコードに反映されます。

    TypeORMの特徴について4点紹介します。

    1点目はTypeScriptでコードが記述できる点です。TypeScrptはフロントエンド開発の現場では必須の言語となっています。 フロントエンドエンジニアが業務を掛け持ちすることで少人数で開発を進めることができます。

    2点目はエンティティクラスを定義することでテーブルに保管されるデータをエンティティクラスのインスタンスとして扱える点です。 データベースへの追加、更新、削除といった操作にSQL文を使ったコードを記述する必要がほとんどありません。 エンティティクラスのインスタンスを操作することでデータベースの操作が行えます。

    3点目はRepositoryパターンが採用できる点です。 データ操作のロジックはリポジトリクラスに記述します。 データ操作のロジックとビジネスロジックを分けてを記述することでプログラムの変更に柔軟に対応できます。

    4点目はマイグレーションがエンティティからの差分を検知する点です。 migrationファイルの生成コマンドを実行すると、既存のテーブルとエンティティとの差分を検知してmigrationファイルが生成されます。 migrationファイル実行コマンドによりSQL文が実行され、データベースの操作が行えます。 マイグレーションによって、エンティティとデータベースの差分を簡単に検知して、データベースに反映させることができます。

    typeorm.png TypeORM公式ページ https://typeorm.io/

    情報

    エンティティとは、実体という意味です。データベースを設計する時にER図を作ります。 ER図はEntity Relationshipの略で、実体と実体の関係性を表す図になります。 一つの実体は1つの箱に入れられ表記されます。その箱がエンティティです。 例えば、Userは顧客IDと名前を持つエンティティとする場合、 次のように記述できます。

    .\proq-text-image\21\21-124.png

    エンティティを元にtypeORMにエンティティクラスを記述すると次のようになります。

    .\proq-text-image\21\21-125.png

    Node.js

    Node.jsとは、JavaScriptコードをOS上で実行するためのランタイム環境です。 開発者がプログラムを記述しコンパイルして実行可能形式のプログラムに変換します。 それを実際にOS上で実行する段階がランタイムです。 ランタイム環境とはモジュールを組み合わせたプログラムを動かすための仕組みです。 Node.jsはJavaScriptコードをOS上で動かすための複数のモジュールで構成されています。 Node.jsを使うことでJavaScriptコードをOS上で実行でき、サーバサイドを構築することができます。

    Node.jsの特徴について3点紹介します。

    1点目はサーバサイドで動作する点です。Node.jsはJavaScripコードを機械語に変換してOS上で実行するといった仕組を持っています。 Node.jsを構成するモジュールであるV8エンジンがコンパイルを担いサーバサイドでの実行環境を作っています。 JavaScriptコードがそのまま使えますのでサーバサイドをJavaScriptで開発できます。

    2点目はサーバを構築できる点です。 サーバとは、HTTPリクエストでクライアントサーバからリクエスト内容を受け取り、 サーバサイドプログラムを実行し、 必要な情報を探してHTTPレスポンスとしてクライアントサーバに返すといった機能です。 Node.jsにはサーバの機能そのものが準備されています。 JavaScriptを使ってサーバを構築することができます。

    3点目はJavaScriptのフレームワークが使える点です。 フレームワークとはよく使われる機能が標準で搭載されいるコンポーネント一式のことです。 フレームワークを使うことで、機能を一から作り上げる必要がなくなります。 Node.jsで利用できるフレームワークとして、Express.js、Koa.js、Nest.jsなどが 挙げられます。 開発者は必要な部分の開発に注力できますので、開発効率を高めることができます。

    node.png Node.js公式ページ https://nodejs.org/ja/

    Nest.js

    Nest.jsはNode.js上で動作するオープンソースのバックエンド開発フレームワークです。 基本的な構造はModule、Controller、Service、Repositoryの4つのコアファイルから構成されています。

    • Module 依存関係の管理
    • Controller ルーティングの処理
    • Service ビジネスロジックの記述
    • Repository データの操作

    それぞれのファイルが持つ役割が分離されているため、プログラムの変更に柔軟に対応できます。 コマンドにはNest.js用のCLIが準備されています。 CLIを用いることでテンプレートファイルの作成が簡単にできます。

    Nest.jsの特徴について2点紹介します。

    1点目はTypeScriptが使える点です。 TypeScriptにはデコレータと呼ばれる特定の役割を持つクラスを作る機能があります。 デコレータによって特定の役割を付与することができます。 例えば、@Contorollerというデコレータを付与することで、ルーティング処理を行うコントローラとしての役割を担う Controllerクラスを宣言できます。 ルーティング機能を担うデコレータ、バリデーション機能を担うデコレータといった 役割に応じたデコレータが実装されていますので、一から基本機能のコードを書く必要がなく、開発効率の向上が図れます。

    2点目はExpress.jsの機能やライブラリを使うことができる点です。 Nest.jsはExpress.jsをコアとして作られています。 Express.jsはMVCモデルを持つフレームワークです。 表示部とデータ部がわかれています。 データ部はコントローラ、モデルといったクラスに別れています。 役割が分業されていますので、プログラムの変更に柔軟に対応できます。

    ランダムな文字列を生成するuuid、非同期通信でデータを取得するaxios、日付操作を行うdayjs、データ管理を行うORM、暗号化処理を行うbcrypt.jsといった ライブラリーを使うことができます。

    nest.png Nest.js公式ページ https://nestjs.com/

    情報

    MVCとは、システム設計の一つのモデルです。 処理機能を担う「Model」、表示と入出力を担う「View」、入力情報に基づきModelとViewを制御する「Controller」の 3つの役割に分離して実装し、それらが連携して処理を進める方式を指します。

    Lesson 2 Chapter 16
    型の拡張

    型の拡張とは、typeで定義した型に別の型を組み合わせて新たな型を定義することや、interfaceで宣言した型を継承して新たな型を宣言するといったことを指します。 本章では、Type、interface、それぞれの型の拡張方法を紹介します。

    type

    typeで定義した型の拡張について見ていきます。 typeは、type 型名 = {型};として型を定義します。無名で作られた型に参照用の型名を与えています。

    次のように文字列型のnameプロパティを持つHumanName型を定義します。

    type HumanName = {
    name: string;
    };

    HumanName型にageプロパティを追加したい場合、 まず、追加したいageプロパティを持つ新たな型HumanAgeを定義ます。

    type HumanAge = {
    age: number;
    };

    次に2つのプロパティを持つ新たな型を定義します。 HumanName型とHumanAge型の2つの型を持つHumanNameAndAge型を定義します。

    type HumanNameAndAge = HumanName & HumanAge;

    type1.png

    HumanName & HumanAgeのように&を使い既存のtypeを組み合わせて指定することで、2つの型を持たせることができます。 このような&を使って複数の型の集合を表す型を交差型と呼びます。typeは交差型を使い型の拡張ができます。

    interface

    次に、interfaceで宣言した型の拡張について見ていきます。 interfaceは、interface 型名 {型}として型を宣言します。interfaceで作られたオブジェクト型に名前をつけることができます。

    次のように文字列型のnameプロパティを持つHuman型を宣言します。

    interface Human {
    name: string;
    }

    interfaceで宣言したHuman型にはageプロパティをマージできます。

    interface Human {
    age: number; //=>マージ可
    }

    Human型を型注釈した変数tomを宣言し、2つのプロパティを持たせることができているかを確認します。

    const tom: Human = {
    name: 'Tom',
    age: 22,
    };

    コンパイルエラーは発生せず、変数tomはnameとageの2つのプロパティを持つことができています。 Human型はnameとageの2つのプロパティを持つオブジェクト型であるとかわります。

    interfaceは継承を使いプロパティを追加できます。

    次のようにnameプロパティを持つHumanName型を宣言します。

    interface HumanName {
    name: string;
    }

    ageプロパティを持たせたい場合、 extendsキーワードを使いHumanName型を継承したHumanNameAndAge型を新たに宣言します。

    interface HumanNameAndAge extends HumanName {
    age: number;
    }

    HumanNameAndAge型を型注釈した変数johnを宣言し、2つのプロパティを持たせることができているかを確認します。

    const john: HumanNameAndAge = {
    name: 'John',
    age: 22,
    };

    コンパイルエラーは発生せず、変数johnはnameとageの2つのプロパティを持つことができています。 HumanNameAndAge型はnameとageの2つのプロパティを持つオブジェクト型であるとかわります。

    typeとinterfaceは型の拡張の仕方が異なっています。 typeは一度定義した型に新しいプロパティをマージできません。継承も使えません。このため交差型を使います。 interfaceは一度宣言した型に新しいプロパティをマージできます。継承を使うこともできます。

    typeとinterfaceはどちらも任意の型を定義するという点では同じです。 ただし、型拡張のしやすさなど、相違点もあります。プロジェクトの特性に応じて適切な型定義を選択してください。

    Lesson 2 Chapter 17
    TypeScriptで開発されるシステム

    TypeScriptで開発されるシステムについて、 本章では、Angular、Vue.js、VSCode、GitHub Desktop、TypeScript、Nest.jsを紹介します。 なお、TypeScript、Nest.jsについては以前の章で説明済みのため、本章では説明を割愛しております。

    Angular

    Webアプリケーションの開発をサポートするオープンソースのフレームワークです。 Googleを始めとする複数の企業や個人を含むコミュニティによって開発されています。 シングルページアプリケーションの開発が可能で、ルーティングや状態管理などフロントエンド開発に必要な機能がそろっています。 ライブラリを追加しなくてもフロントエンド開発を始められる手軽さが強みです。 Web、モバイルWeb、ネイティブモバイル、ネイティブデスクトップ、 どんな端末にも対応しており、あらゆるアプリケーションを構築できます。 JavaScript用のフレームワークとして開発されましたが、バージョン2.0以降ではTypeScriptでの開発が推奨されています。

    Vue.js 3

    GoogleでAngularJSの開発に携わったEvan You氏が開発している、オープンソースのWeb開発用フレームワークです。 AngularJSにインスパイアされ、よりシンプルで自由度が高く、軽量で動作が早いことが特徴です。 1ファイルの中にHTML、CSS、JavaScriptを混在させる単一ファイルコンポーネントを採用しています。 これによりコンポーネント間の関係がつかみやすくなります。 ライブラリの導入については、コアライブラリとサポートライブラリが分かれているため、必要な部分をコンパクトに導入できます。 主にアジア圏での人気が高く日本語の情報も充実しています。 Vue3.0はTypeScriptで記述されており、拡張機能を使用せずにTypeScriptでの開発が可能です。

    VSCode

    VSCodeはMicrosoftによって開発されたWindows、macOSで主に使用されるのソースコードエディタです。 テーマの変更、キーボードショートカット、環境設定の変更、拡張機能のインストール、 デバッグ、シンタックスハイライト、コード補完いった開発効率を向上させる機能を備えています。 軽量テキストエディタで余計な機能は最初から省かれています。 重たい処理であるコーディングを行ってもストレスなく作業を行うことができます。

    GitHub Desktop

    GitHubは開発プロジェクトのソースコードを管理できるWEBサービスです。 ファイルの変更内容の追加、以前の修正内容の確認、ある時点の内容に履歴を戻し作業を再開するといったファイルのバージョン管理ができます。 また、情報の共有のみならず、プロジェクトの進行や課題の管理、ソースコードのレビューなど、チーム開発を向上させる機能を備えています。 GitHub Desktopは、GitHubが提供しているデスクトップ用のアプリケーションです。 GitHub Desktop 3.0ではプルリクエストのチェックや通知を強化しプロジェクトの作業環境を向上させています。

    TypeScript

    第1章で概要記載

    Nest.js

    第9章で概要記載