みなさんTypeScript(JavaScript)でどんなことができるかご存知ですか?初学者向けの言語や大規模開発でよく使う言語だというイメージがあると思います.TypeScript(JavaScript)はフロントの開発からバックの開発までなんでもできる非常に有用な言語です.本記事ではプログラミングをしたことのない人やTypeScript(JavaScript)を触った人のステップアップのためなど幅広い方向けに書きました.また,Node.jsの仕組みやReact,Vue.jsでの応用例やAPI作成まで網羅しました.配列を操作する際の便利な関数やUIライブラルの紹介といった便利機能も記載しています.文法がわからなくなったら適宜文法の欄に戻って確認したり,逆に飛ばしてみたい方はどんどん先に進んで読んでいってください!
他のチートシート
Docker コマンド
git/gh コマンド
lazygit
Go
ステータスコード
SQL
プルリクエスト・マークダウン記法チートシート
ファイル操作コマンドチートシート
Vim
他のシリーズ記事
TypeScriptで学ぶプログラミングの世界
プログラミング言語を根本的に理解するシリーズです.
情報処理技術者試験合格への道 [IP・SG・FE・AP]
情報処理技術者試験の単語集です.
IAM AWS User クラウドサービスをフル活用しよう!
AWSのサービスを例にしてバックエンドとインフラ開発の手法を説明するシリーズです.
本記事の流れ
この記事ではTypeScriptについて以下の流れで説明します.
- TypeScript(JavaScript)の概要
- TypeScriptを学ぶ意義
- TypeScriptとJavaScriptの関係性
- TypeScriptの基本文法
- Helloworldプログラム(変数constの宣言とコンソール出力)
- 再代入可能な変数の定義
- TypeScriptにおける変数とは
- データ型を定義して変数宣言
- リテラルとは
- リテラルの基本概念:
- 列挙型
- 演算記号
- 比較演算子の使い方
- 配列
- 配列の宣言と初期化
- 配列要素へのアクセスと条件付き処理
- 配列のフィルタリングと新しい配列の作成
- 配列要素の追加と削除
- 配列の並べ替え
- 二次元配列
- 二次元配列の処理
- 連想配列
- 配列操作できる便利関数
- map
- filter
- reduce
- find
- indexOf
- lastIndexOf
- findIndex
- forEach
- includes
- push
- pop
- some
- every
- sort
- reverse
- slice
- concat
- join
- flat
- Array.from
- fill
- 条件分岐
- if ... else文
- switch文
- ?演算子
- 条件分岐のネストとは
- 繰り返し処理
- while文
- do ... while文
- for文
- for ... in文
- while文を途中で止めるには
- break文の使用例
- continue文の使用例
- 無限ループとその対策
- 関数
- 関数の定義方法
- 高階関数
- イベントハンドリング
- 関数のデフォルト引数と可変長引数
- 関数のオプション引数と規定値
- 関数のオーバーロード
- 総称型(ジェネリクス関数)
- クロージャー
- オブジェクト指向プログラミング(OOP)とは
- クラス
- クラスの定義
- コンストラクターの定義と使用
- クラスのメソッド定義とオーバーロード
- クラスでのアクセス修飾子
- ゲッターとセッター
- 静的メンバー
- 情報の隠蔽の実装
- クラスの継承とメソッドのオーバーライド
- 抽象クラス
- Node.jsとはなんなのか
- npmってナンダ?
- フレームワークとライブラリの違い
- Node.jsとReact,Vue.js,next.jsの関係性
- Node.jsで何ができるのか
- React VueでのTypeScriptの応用例
- コンポーネントベースアーキテクチャとは
- Reactではどのように書くのか
- 共通コンポーネントの作成
- 各ページ固有のコンポーネントの作成
- 各ページのコンポーネントを指定
- Vue.jsではどのように書くのか
- 共通コンポーネントの作成
- 各ページ固有のコンポーネントの作成
- 各ページのコンポーネントを指定
- ReactやVue.jsからわかるコンポーネントの考え方
- UIライブラリ
- APIをTypeScriptで作成
- バックエンドとはなんなのか
- バックエンドで使う名称
- APIとはなんなのか
- APIの種類
- バックエンドとフロントエンドを繋ぐAPIの処理の流れ
- バックエンドの設計(クリーンアーキテクチャとドメイン駆動設計(DDD))
- Dockerとは
- 実装
- Dockerコンテナ
- domain層entity
- domain層repository
- domain層service)
- application層usecase
- application層interface
- infrastructure層database
- infrastructure層repository
- interfaces層controllers
- interfaces層routes
- エントリポイント
- フロントとバックエンドの連携方法
- バックエンドとはなんなのか
パソコンの方はページの右側に各見出しに遷移できると思うので活用してください!
この記事10万字あるので書いている途中にボケて間違っていることを書いている場合があるかもしれないので,その際は遠慮なくコメントしてください
TypeScript(JavaScript)の概要
はじめにTypeScriptやJavaScriptの概要を説明する.
TypeScriptとは何か.簡潔に言えば,JavaScriptをより扱いやすく,正確に書けるようにしたものと表現できるだろう.
JavaScriptについて聞いたことがある人も少なくないはずだ.JavaScriptは主にWebページに動きを与えるために使われるスクリプト言語で,ルーチンワークの自動化にも活用される.通常はブラウザ上で動作する.
PythonやJavaといった言語はOSに直接アクセスできる言語である.
TypeScriptを学ぶ意義
JavaScriptの特徴として,初心者でも比較的容易にプログラミングできる点が挙げられる.しかし,その気軽さゆえに,厳密性に欠ける面があったり,複雑な処理を表現する際に冗長な記述が必要になったりする.こうした短所は,大規模プロジェクトにおいて特に問題となる.TypeScriptは,このような課題を解決するために誕生した言語である.
TypeScriptとJavaScriptの関係性
TypeScriptは,マイクロソフトが開発したオープンソースの言語である.TypeScriptで書かれたコードは,最終的にJavaScriptのコードに変換される.この過程を一般に「コンパイル」と呼ぶ.
TypeScriptからJavaScriptへの変換はTypeScript専用の実行環境を新たに構築する必要がなく,既存のJavaScript環境でそのまま動作させられるという利点がある.すでにJavaScript向けのライブラリなどが多く用意されているのでそれをそのままTypeScriptに転用できるのは非常に有用だ.
TypeScriptの基本文法
TypeScriptの基本文法はJavaScriptと同じだが,いくつかの重要な機能が追加されている.例えば,変数の型を事前に指定できる静的型付けや,オブジェクト指向プログラミングを容易にするクラスの記述方法の改善がある.さらに,1つの関数で異なる型の引数を扱える総称型や,引数の文字列に応じて異なる関数を呼び出せる文字列オーバーロードといった機能も備えている.
Helloworldプログラム(変数constの宣言とコンソール出力)
本記事では TS PlaygroundでTSを実行する.ここにTSを書き込んで実行すると良い.また,JSに変換したコードも確認することができる.
Runを押すと実行でき,右側の画面のJSを押すとJavaScriptに変換したコードを見ることができる.
はじめにHelloworldプログラムを作成してみよう.
Helloworldとは以下の記事を参考にしてほしい.多くのエンジニアが言語を学ぶ際に初めて作成する,Helloworldは単純にコンソールに"Helloworld"と出力させるだけである.
TS Playgroundこれは初めて開くとすでに以下のコードが入力されている.これがHelloworldプログラムである.
const anExampleVariable = "Hello World";
console.log(anExampleVariable);
このコードではTSもJSもそう大差ないコードであることがわかる.
Runを押して実行すると以下のような画面になる.コンソールに"HelloWorld!"が表示されることがわかる.
Helloworldプログラム(変数constの宣言とコンソール出力)まとめ
再代入できない変数の定義
const str = "apple";
コンソールに出力する方法
console.log(str);
このように変数の中身を出力してソースコードのデバッグをする
コンソールには文字列も出力できる.
console.log("apple");
再代入可能な変数の定義
変数とは
データを入れるための箱のようなもの
TypeScriptにおける変数とは
TypeScriptでは以下のように変数を定義する.
let str;
str = "apple";
console.log(str);
varは再代入できる変数の宣言に使う.constと違って中身を上書きすることができる.
JavaScriptとの違い
let str;
str = "apple";
console.log(srt); //宣言した変数と違うものをコンソールに出力してみる
JSならばエラーとならずTSではエラーになる.JSでは変数を宣言しなくても使えるので,srtという別の変数が新たに作られてしまう.「ReferenceError」と呼ばれる例外が発生してプログラムが終了する.
const let varの違い
letの他にもvarでも宣言できる.一旦const let varの違いを見てみよう.
1. const
定数を宣言するために使用される
-
特徴:
• 再代入不可:一度値を割り当てると,後で変更できない.
• ブロックスコープ:宣言されたブロック内でのみ有効.
• 初期化必須:宣言時に値を割り当てる必要がある. -
使用例:
• 変更されない設定値や定数の宣言に適している.
• 例:const PI = 3.14159;
2. var
最も古い変数宣言方法. -
特徴:
• 関数スコープまたはグローバルスコープを持つ.
• 再代入可能:値を何度でも変更できる.
• 巻き上げ(hoisting):宣言前の使用が可能(undefinedとなる).
• 同名変数の再宣言が可能. -
注意点:
• ブロックスコープを持たないため,予期せぬバグの原因になりやすい.
• 現代のJavaScriptでは使用を避けることが推奨される.
3. let
ES6で導入された変数宣言方法. -
特徴:
• ブロックスコープ:宣言されたブロック内でのみ有効.
• 再代入可能:値を変更できる.
• 同じスコープ内での再宣言は不可.
• Temporal Dead Zone:宣言前の使用でエラーが発生する. -
使用例:
• 値が変更される可能性がある変数の宣言に適している.
• 例:for文のカウンター変数など.
データ型を定義して変数宣言
TypeScriptにおいて,変数を宣言する際にデータ型を明示することが可能だ.変数名の直後にコロンを置き,その後にデータ型を記述するのが一般的な方法である.例えば,金額を保持する変数を宣言する場合,次のように記述できる.
let itemCost: number;
この記法により,変数itemCostはnumber型であることが明確になる.変数の宣言と同時に値を代入することも可能で,その場合は以下のようになる.
let itemCost: number = 980;
複数の変数を一度に宣言することもできる.同じデータ型の変数を複数宣言する場合や,異なるデータ型の変数を同時に宣言する場合がある.例えば,以下のような記述が可能だ.
let width, height: number;
let productName: string, inStock: boolean;
TypeScriptで頻繁に使用されるデータ型には,number(数値),string(文字列),boolean(真偽値),any(任意の型),enum(列挙型)などがある.number,string,booleanはプリミティブ型と呼ばれ,単純な値を表現する.データ型を明示しない場合,変数はany型として扱われる.
実際のコード例を見てみよう.以下は商品の税込価格を計算し表示するプログラムだ.
let basePrice: number = 980;
let taxRate: number = 0.1;
let displayText: string = "円(税込)";
let isIncluded: boolean = false;
if (isIncluded) {
console.log(basePrice + displayText);
} else {
console.log(basePrice * (1 + taxRate) + displayText);
}
このコードでは,basePriceに商品の基本価格,taxRateに税率,displayTextに表示用のテキスト,isIncludedに税込かどうかのフラグを格納している.条件分岐により,税込価格または税抜価格を適切に表示する.
TypeScriptの特筆すべき点は,コンパイル時にデータ型のチェックが行われることだ.これにより,開発段階で型の不一致によるエラーを防ぐことができる.しかし,コンパイル後のJavaScriptコードでは型情報が削除されるため,実行時のパフォーマンスに影響を与えることはない.
変数の初期化時に値を代入すると,TypeScriptは自動的にデータ型を推論する.
例えば,
let price = 980;
と記述すると,priceはnumber型と見なされる.この機能により,コードの簡潔さを保ちつつ,型安全性を確保することができる.
データ型と代入される値の整合性には注意が必要だ.
例えば,
let quantity: number = "10";
のように,数値型の変数に文字列を代入しようとするとエラーが発生する.同様に,文字列型の変数に数値を直接代入することもできない.
TypeScriptの型一覧
TypeScriptの主要なデータ型とその説明,コード例を以下に示す.
-
プリミティブ型:
- number:
整数や浮動小数点数を表す数値型である.
let age: number = 30; let pi: number = 3.14;
- string:
一連の文字を表すテキストデータ型である.
let name: string = "Alice"; let greeting: string = `Hello, ${name}!`;
- boolean:
true または false の真偽値を表す型である.
let isActive: boolean = true; let hasPermission: boolean = false;
- null:
意図的に値が存在しないことを示す特別な値である.
let data: null = null;
- undefined:
値が割り当てられていないことを示す型である.
let result: undefined;
- symbol:
ECMAScript 2015で導入された,一意の識別子を表す型である.
let sym1: symbol = Symbol("key"); let sym2: symbol = Symbol("key"); console.log(sym1 === sym2); // false`
- bigint:
任意精度の整数を表現するための数値型である.
let bigNumber: bigint = 100n; let anotherBigNumber: bigint = BigInt(100);
- number:
-
オブジェクト型:
- object:
プリミティブ型以外のすべての型を表す汎用的な型である.
let user: object = { name: "Bob", age: 25 };
- Array:
同じ型の要素の順序付けられたリストを表す型である.
let numbers: number[] = [1, 2, 3, 4, 5]; let fruits: Array<string> = ["apple", "banana", "orange"];
- Tuple:
固定長の配列で,各要素の型が明示的に指定されている型である.
let pair: [string, number] = ["x", 10];
- Enum:
名前付きの定数セットを定義するために使用される型である.
enum Color { Red, Green, Blue } let c: Color = Color.Green;
- object:
-
特殊型:
- any:
どのような型の値でも許容する,型チェックを無効にする型である.
let data: any = 4; data = "hello"; // OK data = true; // OK
- unknown:
anyより型安全で,使用前に型チェックが必要な型である.
let value: unknown = 10; if (typeof value === "number") { let sum = value + 5; // OK }
- void:
主に関数が値を返さないことを示すために使用される型である.
function logMessage(): void { console.log("Hello"); }
- never:
決して発生しない値の型を表す,特殊な用途に使用される型である.
function throwError(): never { throw new Error("An error occurred"); }
- any:
TypeScriptでの再代入可能な変数定義まとめ
再代入可能な変数の定義
let price = 980;
データ型を定義して変数を定義する
例:number型の場合
let itemCost: number;
もちろんconstでもデータ型を定義できる
リテラルとは
リテラルとは
プログラム中に直接記述される値のことである。変数や定数に代入される前の、「生の」 データ値を指す。プログラミング言語において、リテラルはソースコード上で値を表現する方法であり、その値自体を直接表す記法.つまり変数の対義語であり、変更されないことを前提とした値である.
リテラルの基本概念:
数値リテラル:
数値リテラルには、整数リテラルと浮動小数点リテラルがある。
整数リテラル:
- 10進数: 通常の数字表記だ。例: 42, -789
- 16進数: 0xで始まる。例: 0xCAFE, -0xBEEF
- 2進数: 0bで始まる。例: 0b1010, -0b1100
浮動小数点リテラル:
- 小数点表記: 例: 3.14159, -0.001
- 指数表記: eまたはEを使用する。例: 6.022e23, -2.998E8
let chikyuJinkou: number = 7900000000; // 世界の人口(概算)
let piTi: number = 3.14159265359; // 円周率
let denkoSokudo: number = 2.998E8; // 光速(m/s)
let binsuHyogen: number = 0b1010101; // 2進数表記
let hexHyogen: number = 0xDEADBEEF; // 16進数表記
文字列リテラル:
文字列リテラルは,単一引用符(')または二重引用符(")で囲む.バックティック(`)を使用すると,テンプレートリテラルとなり,複数行の文字列や式の埋め込みが可能となる.
特殊文字はバックスラッシュ(\)でエスケープする.主な特殊文字は以下だ。
- \n: 改行
- \t: タブ
- ': 単一引用符
- ": 二重引用符
- \: バックスラッシュ
- \uXXXX: Unicode文字(16進数で指定)
let sekaiAisatsu: string = "こんにちは、世界!";
let nihongoHyogen: string = '私は"日本語"が好きです。';
let unicodeMoji: string = "\u611B\u60C5"; // "愛情"
let templateBun: string = `
TypeScriptは
強力な言語です。
バージョンは${getTsVersion()}です。
`;
function getTsVersion(): string {
return "4.9.5";
}
列挙型
列挙型(enum)とは
列挙型は、関連する定数の集合を定義するための特別なデータ型だ。TypeScriptでは「enum」キーワードを使用して定義する。列挙型は種類を区別するのに便利であり、コードの可読性と型安全性を向上させる
列挙型の利点
- コードの可読性向上: 数値の代わりに意味のある名前を使用できる。
- 型安全性: 列挙型の変数には、定義された値以外を代入できない。
- 自動補完: IDEで列挙型のメンバーを簡単に参照できる。
列挙型の定義と使用
基本的な列挙型の定義:
enum SEASONS {SPRING, SUMMER, AUTUMN, WINTER};
let season: SEASONS = SEASONS.SUMMER;
console.log(season); // 出力: 1
デフォルトでは、最初の要素に0が割り当てられ、以降は順に1ずつ増加する。
列挙型の値の指定
要素に特定の値を割り当てることも可能だ。
enum SEASONS {SPRING = 1, SUMMER, AUTUMN, WINTER};
この場合、SPRINGは1、SUMMERは2、AUTUMNは3、WINTERは4となる。
全ての要素に個別の値を指定することもできる:
enum SEASONS {SPRING = 1, SUMMER = 2, AUTUMN = 4, WINTER = 8};
列挙型の活用:
条件分岐での使用:
if (season === SEASONS.SUMMER) {
console.log("夏ですね");
}
switch文での使用:
switch (season) {
case SEASONS.SPRING:
console.log("春です");
break;
case SEASONS.SUMMER:
console.log("夏です");
break;
}
ビット演算を活用した列挙型:
列挙型の値を2のべき乗にすることで、ビット演算を活用できる。
enum SEASONS {SPRING = 1, SUMMER = 2, AUTUMN = 4, WINTER = 8};
let springAndSummer = SEASONS.SPRING | SEASONS.SUMMER; // 3
if (springAndSummer & SEASONS.SPRING) {
console.log("春を含みます");
}
リテラルと列挙型まとめ
リテラルは数値リテラルと文字列リテラルがある
(これはTSに限った話ではないので注意Javaでもかけたりする)
列挙型の定義
enum SEASONS {SPRING, SUMMER, AUTUMN, WINTER};
演算記号
数値や文字列を操る際には, 様々な演算記号を駆使しよう.
例えば, 商品の総額を算出する場合, 以下のようなコードを記述できる
var shouhin_nedan, zei_ritsu, sougaku: suuji;
shouhin_nedan = 8000;
zei_ritsu = 0.1;
sougaku = shouhin_nedan * (1 + zei_ritsu);
hyouji(sougaku);
ここでは, 代入や乗算, 加算といった基本的な演算を駆使している.
演算記号の優先順位は数学の法則に従う. 丸括弧内の計算が最優先され, その後に乗算が実行される
演算記号の一覧表を以下に示す. これらの記号は, プログラミングの道具箱だ.
計算に優先順位は上から順である
-
. メンバーの参照
new インスタンスの作成 - () 関数呼び出し
- ++, -- インクリメント, デクリメント(i = i + 1; i = i - 1;を表す)
- !, ~, +, -, typeof, void, delete 単項演算子
- *, /, % 算術演算子 (乗算, 除算, 剰余)
- +, - 算術演算子 (加算, 減算)
- <<, >>, >>> ビットシフト(シフト演算)
- <, <=, >, >=, in, instanceof 比較演算子
- ==, !=, ===, !== 等価演算子
- & bitごとの論理積 (AND)
- ^ bitごとの排他的論理和 (XOR)
- | bitごとの論理和 (OR)
- && 論理積 (AND)
- || 論理和 (OR)
- ? 条件演算子
- yield ジェネレーターの返す値
- *=, +=, -=, =, /=, %= 代入演算子
- , 後ろの式の値を返す
順番が大事な場面は以下のような場合である.
var a, b: number;
b = 10;
a = b += 2; // 「b += 2」が実行された後、「a = b」が実行される
alert("aの値は" + a + "\nbの値は" + b);
注意すべき点として, 0による除算がある. これは数学的には禁じ手だが, プログラム上では「Infinity」という特殊な値となる.
var x, y: suuji;
x = 100;
y = x / 0;
console.log(y); //出力:infinity
また, べき乗の計算には特別な関数を使用することもできる
var kihon, shisuu: number;
kihon = 2;
shisuu = Math.pow(kihon, 10);
console.log(shisuu); //出力:1024
インクリメント,デクリメントとは
「++」と「--」は以下のコードを表す.
i++;
i = i + 1;
i--;
i = i - 1;
「++」と「--」の振る舞いは, その位置によって異なる挙動を示すのだ.
これらの演算子は変数の前後どちらにも配置できるが, その効果は微妙に異なる.
-
前置型の場合:
変数の値が即座に変更され, その新しい値が返される.let kazu: number = 5; let kekka: number; kekka = ++kazu; console.log("kazu: " + kazu); // 出力: kazu: 6 console.log("kekka: " + kekka); // 出力: kekka: 6
この例では, kazuの値が6に増加し, その新しい値がkekkaに代入される.
-
後置型の場合:
変数の値は変更されるが, 返される値は変更前の元の値である.let kazu: number = 5; let kekka: number; kekka = kazu++; console.log("kazu: " + kazu); // 出力: kazu: 6 console.log("kekka: " + kekka); // 出力: kekka: 5
この例では, kazuの値は6に増加するが, kekkaには増加前の5が代入される.
「--」演算子も同様の法則に従う. 以下に簡単な例を示す:
let kazu: number = 10;
console.log(--kazu); // 出力: 9 (前置型: 即座に減少し, 新しい値を返す)
console.log(kazu); // 出力: 9
console.log(kazu--); // 出力: 9 (後置型: 元の値を返し, その後減少)
console.log(kazu); // 出力: 8
比較演算子の使い方
日常的な感覚で「18歳以上25歳未満」を表現しようとして,
以下のようなコードを書いてしまう人は少なくない:
let nenrei: number = 30;
if (18 <= nenrei < 25) {
console.log("対象年齢です");
}
一見正しそうに見えるこの記述だが, TypeScriptではエラーとなり,
JavaScriptでは予期せぬ動作をする.
これは「18 <= nenrei」が先に評価され, その結果(true)と25が比較される.
JavaScriptではtrueが1に変換されるため, 常に真となってしまうのだ.
正しい記述は以下の通りだ:
if (nenrei >= 18 && nenrei < 25) {
console.log("対象年齢です");
}
演算記号まとめ
- 演算記号は数値や文字列操作の基本ツール
- 優先順位あり,括弧内が最優先
- 基本演算(+,-,*,/)に加え,インクリメント(++) やデクリメント(--) 等がある
- 比較演算子使用時は論理演算子(&&, ||) で複数条件を正しく結合すること
- 0除算はInfinityを返す
配列
配列とは
プログラミングにおいて同じデータ型の複数の要素を順序付けて格納するデータ構造
配列の宣言と初期化
const prefectures: string[] = [
"北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県",
"茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県",
"新潟県", "富山県", "石川県", "福井県", "山梨県", "長野県", "岐阜県",
"静岡県", "愛知県", "三重県", "滋賀県", "京都府", "大阪府", "兵庫県",
"奈良県", "和歌山県", "鳥取県", "島根県", "岡山県", "広島県", "山口県",
"徳島県", "香川県", "愛媛県", "高知県", "福岡県", "佐賀県", "長崎県",
"熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県"
];
この配列は日本の47都道府県を格納している。各要素は文字列型で、配列のインデックスは0から始まる。
配列要素へのアクセスと条件付き処理
console.log("特別な行政区分を持つ都道府県:");
for (let i = 0; i < prefectures.length; i++) {
if (prefectures[i].includes("都") || prefectures[i].includes("府")) {
console.log(`${i + 1}番目: ${prefectures[i]}`);
}
}
この例では、配列の各要素にアクセスし、「都」または「府」を含む都道府県を出力している。includesメソッドを使用して文字列の一部を検索している。
配列のフィルタリングと新しい配列の作成
const tohokuRegion = prefectures.filter((pref, index) => index >= 1 && index <= 6);
console.log("東北地方の県:", tohokuRegion);
filterメソッドを使用して、インデックスに基づいて東北地方の県だけを抽出し、新しい配列を作成している。
配列要素の追加と削除
prefectures.push("日本国外");
console.log("追加後の最後の要素:", prefectures[prefectures.length - 1]);
const removedPrefecture = prefectures.pop();
console.log("削除された要素:", removedPrefecture);
pushメソッドで配列の末尾に新しい要素を追加し、popメソッドで最後の要素を削除している。
配列の並べ替え
const sortedPrefectures = [...prefectures].sort((a, b) => a.localeCompare(b, 'ja'));
console.log("五十音順に並べ替えた最初の5都道府県:", sortedPrefectures.slice(0, 5));
スプレッド構文(...)を使用して配列のコピーを作成し、sortメソッドで五十音順に並べ替えている。localeCompareメソッドを使用して日本語の正しい順序で比較している。
二次元配列
地方ごとの都道府県
const regionPrefectures: string[][] = [
["北海道"],
["青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県"],
["茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県"],
["新潟県", "富山県", "石川県", "福井県", "山梨県", "長野県", "岐阜県", "静岡県", "愛知県"],
["三重県", "滋賀県", "京都府", "大阪府", "兵庫県", "奈良県", "和歌山県"],
["鳥取県", "島根県", "岡山県", "広島県", "山口県"],
["徳島県", "香川県", "愛媛県", "高知県"],
["福岡県", "佐賀県", "長崎県", "熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県"]
];
これは地方ごとに都道府県をグループ化した二次元配列である。各内部配列が一つの地方を表している。
二次元配列の処理
各地方の都道府県数を表示
const regions = ["北海道", "東北", "関東", "中部", "近畿", "中国", "四国", "九州・沖縄"];
for (let i = 0; i < regionPrefectures.length; i++) {
console.log(`${regions[i]}地方: ${regionPrefectures[i].length}都道府県`);
}
この例では、二次元配列と別の配列(地方名)を組み合わせて使用している。外側のループは各地方を処理し、内部配列のlengthプロパティを使って各地方の都道府県数を取得している。
for文の説明は以下で説明する
連想配列
連想配列とは
キーと値のペアを格納するデータ構造であり,TypeScriptではオブジェクトリテラルを使用して実装できる.
果物とその色の連想配列の例
let fruits = {
apple: "red",
banana: "yellow",
grape: "purple"
};
値へのアクセス
console.log(fruits["apple"]); // "red"を出力
console.log(fruits.banana); // "yellow"を出力
新しい要素の追加
fruits["orange"] = "orange";
要素の削除
delete fruits.grape;
連想配列の走査
for (let fruit in fruits) {
console.log(`${fruit}: ${fruits[fruit]}`);
}
インデックスシグネチャを用いたデータ型の指定
interface TeamMembers {
[role: string]: string;
}
let team: TeamMembers = { coach: "佐藤", captain: "鈴木" };
team["ace"] = "田中"; // OK
// team["player1"] = 7; // エラー: 数値は代入できない
interfaceを用いたインデックスシグネチャの定義:
interface NumberDictionary {
[index: string]: number;
}
let playerNumbers: NumberDictionary = {};
playerNumbers["ace"] = 1;
playerNumbers["captain"] = 4;
// playerNumbers["coach"] = "佐藤"; // エラー: 文字列は代入できない
Array型を用いた連想配列の作成:
let team: string[] = new Array(9);
team["coach"] = "佐藤";
team["captain"] = "鈴木";
console.log(team["coach"]); // "佐藤"
配列まとめ
TypeScriptにおける配列は多様な操作が可能な強力なデータ構造である.
配列は同じ型の要素を順序付けて格納し,インデックスを用いてアクセスできる.
配列の宣言と初期化は簡単で,角括弧を使用して要素をカンマ区切りで列挙する.配列のサイズは動的に変更可能であり,要素の追加や削除が容易に行える.
配列の要素へのアクセスは,インデックスを使用して行う.インデックスは0から始まり,配列の長さより1小さい数まで続く.配列の長さはlengthプロパティで取得でき,これを用いてループ処理を行うことが一般的である.配列の要素は条件に基づいてフィルタリングしたり,特定の基準で並べ替えたりすることができる.
また,配列は多次元にすることも可能で,二次元配列を使用することで複雑なデータ構造を表現できる.二次元配列は,配列の要素としてさらに配列を持つ構造であり,行列やグリッドのようなデータを扱うのに適している.
配列操作には様々なビルトインメソッドが用意されており,これらを活用することで効率的にデータを処理できる.配列は反復処理やデータの集約,変換など,多くのプログラミングタスクで中心的な役割を果たす重要なデータ構造である.
配列操作できる便利関数
TypeScriptの配列には、様々な便利な関数が用意されている.ここでは復習ついでにJavaScriptバージョンも記述しておく.TypeScriptとの違いを感じてほしい.
map
配列の各要素に対して指定された関数を適用し,新しい配列を返す.
JavaScript
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubledNumbers); // [2, 4, 6, 8, 10]
TypeScript
const numbers: number[] = [1, 2, 3, 4, 5];
const doubledNumbers: number[] = numbers.map((num: number) => num * 2);
console.log(doubledNumbers); // [2, 4, 6, 8, 10]
filter
配列の要素をフィルタリングし,指定された条件に合う要素だけを含む新しい配列を返す.
JavaScript
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4]
TypeScript
const numbers: number[] = [1, 2, 3, 4, 5];
const evenNumbers: number[] = numbers.filter((num: number) => num % 2 === 0);
console.log(evenNumbers); // [2, 4]
reduce
配列の要素を累積的に処理し,単一の値を返す.
JavaScript
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((acc, num) => acc + num, 0);
console.log(sum); // 15
TypeScript
const numbers = [1, 2, 3, 4, 5];
const sum: number = numbers.reduce((acc: number, num: number) => acc + num, 0);
console.log(sum); // 15
find
配列内で指定された条件に合う最初の要素を返す.
JavaScript
const fruits = ['apple', 'banana', 'orange'];
const foundFruit = fruits.find(fruit => fruit.startsWith('b'));
console.log(foundFruit); // 'banana'
TypeScript
const fruits: string[] = ['apple', 'banana', 'orange'];
const foundFruit: string | undefined = fruits.find((fruit: string) => fruit.startsWith('b'));
console.log(foundFruit); // 'banana'
indexOf
指定された要素の最初のインデックスを返す
JavaScript
const fruits = ['apple', 'banana', 'orange'];
const bananaIndex = fruits.indexOf('banana');
console.log(bananaIndex); // 1
TypeScript
const fruits: string[] = ['apple', 'banana', 'orange'];
const bananaIndex: number = fruits.indexOf('banana');
console.log(bananaIndex); // 1
lastIndexOf
指定された要素の最後のインデックスを返す
JavaScript
const repeatedNumbers = [1, 2, 3, 2, 1];
const lastIndexOfTwo = repeatedNumbers.lastIndexOf(2);
console.log(lastIndexOfTwo); // 3
TypeScript
const repeatedNumbers: number[] = [1, 2, 3, 2, 1];
const lastIndexOfTwo: number = repeatedNumbers.lastIndexOf(2);
console.log(lastIndexOfTwo); // 3
findIndex
配列内で指定された条件に合う最初の要素のインデックスを返す.
const numbers = [1, 2, 3, 4, 5];
const index = numbers.findIndex(num => num > 3);
console.log(index); // 3
const numbers: number[] = [1, 2, 3, 4, 5];
const index: number = numbers.findIndex((num: number) => num > 3);
console.log(index); // 3
findとfindindexとindexOfの違い
indexOfの引数には探したい値のみを入れるがfindindexは探す際の条件式を入れる.findはそもそもインデックスではなく要素が返ってくる.
//indexOf
const bananaIndex: number = fruits.indexOf('banana'); // 3
//findindex
const index: number = numbers.findIndex((num: number) => num > 3); // 3
//find
const foundFruit: string | undefined = fruits.find((fruit: string) => fruit.startsWith('b')); // banana
forEach
配列の各要素に対して指定された関数を実行する.
javaScript
const fruits = ['apple', 'banana', 'orange'];
fruits.forEach(fruit => console.log(fruit));
// 'apple'
// 'banana'
// 'orange'
TypeScript
const fruits: string[] = ['apple', 'banana', 'orange'];
fruits.forEach((fruit: string) => console.log(fruit));
// 'apple'
// 'banana'
// 'orange'
includes
配列に指定された要素が含まれているかどうかを真偽値で返す.
JavaScript
const fruits = ['apple', 'banana', 'orange'];
console.log(fruits.includes('banana')); // true
console.log(fruits.includes('grape')); // false
TypeScript
const fruits: string[] = ['apple', 'banana', 'orange'];
console.log(fruits.includes('banana')); // true
console.log(fruits.includes('grape')); // false
push
配列の末尾に1つ以上の要素を追加する
JavaScript
const fruits = ['apple', 'banana', 'orange'];
fruits.push('grape');
console.log(fruits); // ['apple', 'banana', 'orange', 'grape']
TypeScript
const fruits: string[] = ['apple', 'banana', 'orange'];
fruits.push('grape');
console.log(fruits); // ['apple', 'banana', 'orange', 'grape']
pop
配列の最後の要素を削除し、その要素を返す
JavaScript
const fruits = ['apple', 'banana', 'orange'];
const lastFruit = fruits.pop();
console.log(lastFruit); // 'grape'
console.log(fruits); // ['apple', 'banana', 'orange']
TypeScript
const fruits: string[] = ['apple', 'banana', 'orange'];
const lastFruit: string | undefined = fruits.pop();
console.log(lastFruit); // 'grape'
console.log(fruits); // ['apple', 'banana', 'orange']
some
配列の少なくとも1つの要素が指定された条件を満たすかどうかを真偽値で返す.
JavaScript
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.some(num => num > 3)); // true
console.log(numbers.some(num => num > 10)); // false
TypeScript
const numbers: number[] = [1, 2, 3, 4, 5];
console.log(numbers.some(num => num > 3)); // true
console.log(numbers.some(num => num > 10)); // false
every
配列のすべての要素が指定された条件を満たすかどうかを真偽値で返す.
JavaScript
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.every(num => num > 0)); // true
console.log(numbers.every(num => num > 3)); // false
TypeScipt
const numbers: number[] = [1, 2, 3, 4, 5];
console.log(numbers.every((num: number) => num > 0)); // true
console.log(numbers.every((num: number) => num > 3)); // false
sort
配列の要素を並べ替える.デフォルトでは,要素を文字列に変換して辞書順に並べ替える.
JavaScript
const fruits = ['banana', 'apple', 'orange'];
fruits.sort();
console.log(fruits); // ['apple', 'banana', 'orange']
TypeScript
const fruits: string[] = ['banana', 'apple', 'orange'];
const sortedFruits: string[] = [...fruits].sort();
console.log(sortedFruits); // ['apple', 'banana', 'orange']
reverse
配列の要素の順序を反転
JavaScript
const numbers = [1, 2, 3, 4, 5];
numbers.reverse();
console.log(numbers); // [5, 4, 3, 2, 1]
TypeScript
const numbers: number[] = [1, 2, 3, 4, 5];
const reversedNumbers: number[] = [...numbers].reverse();
console.log(reversedNumbers); // [5, 4, 3, 2, 1]
slice
配列の一部を取り出して新しい配列を返す.
JavaScript
const numbers = [1, 2, 3, 4, 5];
const slicedNumbers = numbers.slice(1, 4);
console.log(slicedNumbers); // [2, 3, 4]
TypeScript
const numbers: number[] = [1, 2, 3, 4, 5];
const slicedNumbers: number[] = numbers.slice(1, 4);
console.log(slicedNumbers); // [2, 3, 4]
concat
複数の配列を結合して新しい配列を返す.
JavaScript
const array1 = [1, 2, 3];
const array2 = [4, 5, 6];
const concatenatedArray = array1.concat(array2);
console.log(concatenatedArray); // [1, 2, 3, 4, 5, 6]
TypeScript
const array1: number[] = [1, 2, 3];
const array2: number[] = [4, 5, 6];
const concatenatedArray: number[] = array1.concat(array2);
console.log(concatenatedArray); // [1, 2, 3, 4, 5, 6]
join
配列の要素を指定された区切り文字で結合して文字列を返す.
JavaScript
const fruits = ['apple', 'banana', 'orange'];
const joinedString = fruits.join(', ');
console.log(joinedString); // 'apple, banana, orange'
TypeScript
const fruits: string[] = ['apple', 'banana', 'orange'];
const joinedString: string = fruits.join(', ');
console.log(joinedString); // 'apple, banana, orange'
flat
多次元配列を平坦化して新しい配列を返す.
JavaScript
const nestedArray = [1, [2, 3], [4, [5, 6]]];
const flattenedArray = nestedArray.flat(2);
console.log(flattenedArray); // [1, 2, 3, 4, 5, 6]
TypeScript
const nestedArray: (number | number[])[] = [1, [2, 3], [4, [5, 6]]];
const flattenedArray: number[] = nestedArray.flat(2) as number[];
console.log(flattenedArray); // [1, 2, 3, 4, 5, 6]
Array.from
配列様のオブジェクトや反復可能なオブジェクトから新しい配列を作成
JavaScript
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
const newArray = Array.from(arrayLike);
console.log(newArray); // ['a', 'b', 'c']
TypeScript
const arrayLike: { [key: number]: string, length: number } = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
const newArray: string[] = Array.from(arrayLike);
console.log(newArray); // ['a', 'b', 'c']
fill
配列の要素を指定された値で埋める
JavaScript
const filledArray = new Array(5).fill(0);
console.log(filledArray); // [0, 0, 0, 0, 0]
TypeScript
const filledArray: number[] = new Array(5).fill(0);
console.log(filledArray); // [0, 0, 0, 0, 0]
条件分岐
変数や式の値によって異なる文を実行したり,異なる値を返したりするためには,以下のような方法が使える.機能の違いが用途の違いにそのまま結び付いているので,それほど意識しなくても使えるが,併せて特徴も示しておく.
if ... else文
if ... else文は,式の値によって異なる文を実行する.if文を組み合わせ,異なる変数の値を調べて多分岐させることもできる.
一般的な形式:
if (条件式1) {
// 条件式1がtrueの場合に実行される文
} else if (条件式2) {
// 条件式2がtrueの場合に実行される文
} else {
// それ以外の場合に実行される文
}
コード例:
let age = 20;
let message: string;
if (age >= 18) {
message = "成人です.";
} else {
message = "未成年です.";
}
console.log(message);// "成人です"が出力される
switch文
switch文は,変数の値によって異なる文を実行する.1つの変数の値を調べて多分岐させるときに便利である.
一般的な形式:
switch (式) {
case 値1:
// 式の値が値1に一致した場合に実行される文
break;
case 値2:
// 式の値が値2に一致した場合に実行される文
break;
default:
// どのcaseにも一致しなかった場合に実行される文
コード例:
let dayOfWeek = 3;
let dayName: string;
switch (dayOfWeek) {
case 1:
dayName = "月曜日";
break;
case 2:
dayName = "火曜日";
break;
case 3:
dayName = "水曜日";
break;
default:
dayName = "その他の曜日";
}
console.log(dayName);
}
?演算子
?演算子(条件演算子)は,条件式の値によって,異なる式の値を返す.比較的単純な条件分岐を簡潔に書くときに便利である.
一般的な形式:
条件式 ? 真の場合の式 : 偽の場合の式
コード例:
let temperature = 25;
let weatherStatus = temperature > 30 ? "暑い" : "快適";
console.log(weatherStatus);
これらの条件分岐方法は,それぞれ異なる状況で有用である.if ... else文は複雑な条件分岐に適しており,switch文は単一の変数に基づく多分岐に適している.?演算子は簡潔な条件分岐を一行で書くのに適している.
==演算子と===演算子の違い
TypeScriptには等価比較のための==演算子と===演算子がある
-
==演算子(等価演算子)
- 値が等しければtrueを返す
- 必要に応じてデータ型を変換する
-
===演算子(厳密等価演算子)
- 値とデータ型が両方等しい場合のみtrueを返す
- データ型の変換を行わない
例:
let a = 5;
let b = "5";
console.log(a == b); // true
console.log(a === b); // false
===演算子は型の安全性を高めるため,多くの場合で推奨される.しかし,状況に応じて適切な演算子を選択することが重要である.
オブジェクトの比較の場合,両演算子とも参照の同一性をチェックする.
let obj1 = {value: 5};
let obj2 = {value: 5};
let obj3 = obj1;
console.log(obj1 == obj2); // false
console.log(obj1 === obj2); // false
console.log(obj1 === obj3); // true
条件分岐のネストとは
ネストとはi
f ... else文の中に別のif ... else文を入れ子状に記述すること.これにより,複数の条件を組み合わせて複雑な分岐処理を実現できる.
-
2分岐のネスト
複数の条件を順番に判定する場合に使用する
例:ユーザーの年齢と会員ステータスによって割引率を決定する。
let age: number = 25;
let isMember: boolean = true;
let discount: number;
if (age >= 60) {
if (isMember) {
discount = 0.2; // 60歳以上の会員
} else {
discount = 0.1; // 60歳以上の非会員
}
} else {
if (isMember) {
discount = 0.1; // 60歳未満の会員
} else {
discount = 0.05; // 60歳未満の非会員
}
}
console.log(`割引率: ${discount * 100}%`);
-
else if によるネスト
複数の条件を順番に判定し、最初に真となる条件の処理を実行する。
例:試験の点数によって成績を判定する。
let score: number = 85;
let grade: string;
if (score >= 90) {
grade = "A";
} else if (score >= 80) {
grade = "B";
} else if (score >= 70) {
grade = "C";
} else if (score >= 60) {
grade = "D";
} else {
grade = "F";
}
console.log(`あなたの成績は ${grade} です。`);
これらのネストを使用することで、複雑な条件分岐を実現できる。
ただし、過度にネストを深くすると可読性が低下するため、適切な使用が求められる。
条件分岐まとめ
- 条件分岐には主に3種類ある
- if...else文:複雑な条件に適す
- switch文:単一変数の多分岐に便利
- ?演算子:簡潔な一行条件分岐に使用
- ネスト構造で複雑な分岐も可能だが,可読性に注意が必要
繰り返し処理
TypeScriptには主に4つの繰り返し処理の方法がある. それぞれの特徴と使用例を見ていく.
while文
条件がtrueである間,処理を繰り返す.
例: 偶数のみを出力する
let num = 0;
while (num <= 10) {
if (num % 2 === 0) {
console.log(num);
}
num++;
}
do ... while文
処理を最低1回実行し,その後条件がtrueである間繰り返す.
例: ランダムな数を生成し,5が出るまで繰り返す
let randomNum;
do {
randomNum = Math.floor(Math.random() * 10) + 1;
console.log("生成された数: " + randomNum);
} while (randomNum !== 5);
for文
初期化,条件,更新をまとめて記述できる. 回数が決まっている繰り返しに適している.
例: フィボナッチ数列の最初の10項を計算する
let fib = [0, 1];
for (let i = 2; i < 10; i++) {
fib[i] = fib[i-1] + fib[i-2];
console.log("フィボナッチ数列の第" + (i+1) + "項: " + fib[i]);
}
for ... in文
オブジェクトのプロパティを順に処理する.
例: 果物の在庫を表示する
let fruitInventory = {
りんご: 5,
バナナ: 3,
オレンジ: 2,
ぶどう: 4
};
for (let fruit in fruitInventory) {
console.log(fruit + "の在庫: " + fruitInventory[fruit] + "個");
}
while文を途中で止めるには
while文による繰り返しはbreak comtinueを使って繰り返しを止めることができる.
break文の使用例
break文は, 特定の条件が満たされた時に繰り返しを即座に終了させる. 以下は, サイコロを振って6が出るまでの回数を数えるプログラムで, 1が出たらゲームオーバーとする例である.
let count: number = 0;
let d: number;
while ((d = dice()) !== 6) {
if (d === 1) {
console.log("NGな目が出ました");
break; // 1が出たらその時点で繰り返しを終了
}
count++;
}
console.log(`サイコロを振った回数: ${count}`);
function dice(): number {
return Math.floor(Math.random() * 6) + 1;
}
このプログラムでは, 1が出た時点でbreak文が実行され, while文の繰り返しが終了する.
continue文の使用例
continue文は, 現在の繰り返しをスキップして次の繰り返しに進む. 以下は, サイコロを振って6が出るまでの回数を数える際, 同じ目が連続して出た場合は1回と数える例である.
let count: number = 0;
let d: number;
let prev: number = 0;
let dup: number = 0;
while ((d = dice()) !== 6) {
if (prev === d) {
dup++;
continue; // 前回と同じ目なら数えずに次の繰り返しへ
}
count++;
prev = d;
}
console.log(`カウントした回数: ${count}, 重複した回数: ${dup}`);
function dice(): number {
return Math.floor(Math.random() * 6) + 1;
}
このプログラムでは, 前回と同じ目が出た場合にcontinue文が実行され, カウントを増やさずに次の繰り返しに進む.
これらの文を適切に使用することで, より柔軟で効率的な繰り返し処理を実現できる. break文は条件に応じて繰り返しを早期終了させ, continue文は特定の条件下で処理をスキップする. どちらも, プログラムの流れを細かく制御するのに役立つ重要な制御構文である.
無限ループとその対策
無限ループは,条件が常にtrueとなってしまい,プログラムが終了しない状態を指す. これはしばしばバグの原因となり,プログラムのパフォーマンスに重大な影響を与える可能性がある.
例1: 条件が常にtrueになる場合
while (true) {
console.log("This will never end!");
}
例2: 条件が更新されない場合
let x = 0;
while (x < 10) {
console.log(x);
// xが更新されないので,永遠に0が出力され続ける
}
例3: 不適切な条件更新
for (let i = 0; i > -1; i++) {
console.log(i);
// iは常に正の数になるため,永遠にループが続く
}
対策としては以下の例が挙げられる
-
ループ条件の確認: ループの条件が適切に設定されているか,必ず偽になる状況があるか確認する.
-
適切な更新: ループ内で条件に使用している変数が適切に更新されているか確認する.
-
break文の使用: 特定の条件でループを抜け出すためのbreak文を適切に配置する.
-
最大繰り返し回数の設定: 安全策として,最大繰り返し回数を設定し,それを超えた場合にループを強制終了する.
例:
let i = 0;
while (true) {
console.log(i);
i++;
if (i >= 10) {
break; // iが10以上になったらループを抜ける
}
}
例:
const MAX_ITERATIONS = 1000000;
let count = 0;
while (someCondition) {
// 処理
count++;
if (count > MAX_ITERATIONS) {
console.error("最大繰り返し回数を超えました");
break;
}
}
繰り返し処理方法まとめ
4種類の繰り返し文がある.
while文・do...while文・for文・for...in文
- while文とfor文は条件が真の間繰り返し
- do...while文は最低1回実行後に条件を確認する
- for...in文はオブジェクトのプロパティを処理する
- break文とcontinue文を使用して繰り返しの制御が可能
無限ループは条件設定ミスで発生し,適切な条件設定,変数更新,break文の使用,最大繰り返し回数の設定などで対策できる.
関数
関数とは
ひとまとまりの処理を記述し,名前を付けたものである.
関数の基本:
関数は入力(引数)を受け取り,処理を行い,結果(戻り値)を返す.
function add(a: number, b: number): number {
return a + b;
}
const result = add(3, 5); // result は 8
この例では,add
関数が2つの数値を受け取り,その和を返している.TypeScriptでは,引数と戻り値の型を明示的に指定できる.
関数の利点
- 再利用性: 一度定義すれば,複数回使用できる.
- 抽象化: 内部の詳細を知らなくても使用できる.
- コードの整理: 関連する処理をまとめることで,コードの構造が明確になる.
関数の定義方法
1.関数宣言
function greet(name: string): void {
console.log(`こんにちは,${name}さん`);
}
2. 関数式
const greet = function(name: string): void {
console.log(`こんにちは,${name}さん`);
};
3. アロー関数式
const greet = (name: string): void => {
console.log(`こんにちは,${name}さん`);
};
これらの方法は,状況に応じて使い分ける.関数宣言は巻き上げ(hoisting)の対象 となるが,関数式とアロー関数式は巻き上げの対象とならない.
巻き上げ(hoisting)とは
JavaScriptやTypeScriptにおける変数宣言や関数宣言の振る舞いを指す用語である.具体的には,これらの宣言がコード内の実際の位置に関わらず,スコープの先頭に移動されたかのように扱われる現象を指す.
-
巻き上げの影響:
- コードの可読性を下げる可能性がある.
- 予期せぬバグの原因となることがある.
- スコープの理解を難しくする.
-
巻き上げの対処法:
- 変数は使用する直前に宣言・初期化する.
-
let
やconst
を使用し,var
の使用を避ける. - 関数宣言は,使用する前に行う.
1. 変数の巻き上げ:
console.log(x); // undefined
var x = 5;
console.log(x); // 5
上記のコードは,実際には以下のように解釈される:
var x;
console.log(x); // undefined
x = 5;
console.log(x); // 5
変数宣言(var)は巻き上げられるが,初期化は巻き上げられない.
2. 関数宣言の巻き上げ:
sayHello(); // "Hello!"
function sayHello() {
console.log("Hello!");
}
関数宣言は全体が巻き上げられるため,宣言前に呼び出すことができる.
3. let と const:
ES6で導入されたlet
とconst
は巻き上げの対象とはならない.
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 10;
4. 関数式の巻き上げ:
関数式やアロー関数は巻き上げの対象とならない.
greet(); // TypeError: greet is not a function
var greet = function() {
console.log("Hi!");
};
この場合,変数greet
の宣言は巻き上げられるが,関数の代入は巻き上げられない.
高階関数
関数をオブジェクトとして扱い,他の関数に渡すことができる.
function operateOnNumbers(a: number, b: number, operation: (x: number, y: number) => number): number {
return operation(a, b);
}
const sum = operateOnNumbers(5, 3, (x, y) => x + y); // sum は 8
const product = operateOnNumbers(5, 3, (x, y) => x * y); // product は 15
// より複雑な操作も可能
const power = operateOnNumbers(2, 3, (x, y) => Math.pow(x, y)); // power は 8
この例では,operateOnNumbers
関数が2つの数値と,それらに対する操作を行う関数を引数として受け取っている.これにより,同じ関数を用いて異なる演算を行うことができる.
イベントハンドリング
Webアプリケーションでは,関数をイベントハンドラーとして使用することが多い.
const button = document.querySelector('button');
const input = document.querySelector('input');
button.addEventListener('click', () => {
const inputValue = input.value;
console.log(`入力された値: ${inputValue}`);
alert(`こんにちは,${inputValue}さん!`);
});
この例では,ボタンのクリックイベントに対してアロー関数をハンドラーとして設定している.ユーザーがボタンをクリックすると,入力フィールドの値を取得し,コンソールに出力した後,アラートで表示する.
関数のデフォルト引数と可変長引数
TypeScriptでは,関数のデフォルト引数や可変長引数を使用できる.
function createGreeting(name: string, greeting: string = "こんにちは"): string {
return `${greeting},${name}さん!`;
}
console.log(createGreeting("太郎")); // "こんにちは,太郎さん!"
console.log(createGreeting("花子", "おはよう")); // "おはよう,花子さん!"
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
関数まとめ Part1
-
関数は,ひとまとまりの処理を記述し名前を付けたものである.関数は入力(引数)を受け取り,処理を行い,結果(戻り値)を返す.
-
関数の定義方法には,
- 関数宣言
- 関数式
- アロー関数式
がある.
-
関数宣言は巻き上げの対象となるが,関数式とアロー関数式は巻き上げの対象とならない.
巻き上げとは,変数宣言や関数宣言がコード内の実際の位置に関わらず,スコープの先頭に移動されたかのように扱われる現象を指す.
-
高階関数は,関数を引数として受け取ったり,関数を戻り値として返したりする関数である.これにより,より柔軟な処理が可能になる.
-
イベントハンドリングでは,関数をイベントのリスナーとして使用する.また,デフォルト引数や可変長引数を使用することで,より柔軟な関数設計が可能である.
関数のオプション引数と規定値
TypeScriptにおいて,関数のオプション引数と既定値は柔軟な関数設計を可能にする重要な機能である.これらを活用することで,関数の汎用性が高まり,様々な状況に対応できるようになる.
以下に,ユーザーのプロフィールを作成する関数の例を示す.
function createUserProfile(
name: string,
age: number,
occupation?: string,
hobbies: string[] = []
): { name: string; age: number; occupation: string; hobbies: string[] } {
return {
name,
age,
occupation: occupation || "未設定",
hobbies
};
}
console.log(createUserProfile("山田太郎", 30));
console.log(createUserProfile("佐藤花子", 25, "エンジニア"));
console.log(createUserProfile("鈴木一郎", 35, "医師", ["読書", "旅行"]));
この関数では,nameとageは必須の引数である.occupationはオプションの引数であり,?を使用して指定する.hobbiesは既定値として空の配列を持つ.
関数内では,occupationが提供されない場合に"未設定"というデフォルト値を使用している.これは,オプション引数の別の処理方法を示している.
この関数を呼び出す際,必要に応じて引数を省略したり,全ての引数を指定したりすることができる.例えば,最初の呼び出しではoccupationとhobbiesを省略し,2番目の呼び出しではhobbiesのみを省略している.
関数のオーバーロード
関数のオーバーロードとは,同じ名前の関数を複数定義し,引数の型や数が異なる場合に別々の処理を行えるようにする機能である.これにより,関数の柔軟性と再利用性が向上する.
function calculateArea(shape: string, a: number): number; //円の面積計算用
function calculateArea(shape: string, a: number, b: number): number; // 長方形の面積計算用
function calculateArea(shape: string, a: number, b?: number): number { //実際の関数定義(b?として引数bをオプションとしている)
if (shape === "circle") {
return Math.PI * a * a;
} else if (shape === "rectangle" && b !== undefined) {
return a * b;
} else {
throw new Error("Invalid parameters");
}
}
console.log(calculateArea("circle", 5)); // 78.53981633974483(5*5*Math.PI)
console.log(calculateArea("rectangle", 4, 6)); // 24
この例では,calculateArea関数が2つのオーバーロードを持つ.1つ目は円の面積を計算するためのもので,図形の種類と半径を受け取る.2つ目は長方形の面積を計算するためのもので,図形の種類と幅,高さを受け取る.関数の実装では,shapeパラメータで図形の種類を判断する.円の場合はaを半径としてπr^2の公式で面積を計算し,長方形の場合はaを幅,bを高さとしてa * bで面積を計算する.それ以外の場合はエラーを投げる.
関数の呼び出しでは,calculateArea("circle", 5)で円の面積を計算し,calculateArea("rectangle", 4, 6)で長方形の面積を計算している.実行結果は2行となり,1行目は円の面積,2行目は長方形の面積を示す.円の面積は約78.53981633974483となる.これは半径5の円の面積がπ * 5^2であることに基づく.
小数点以下の桁数が多いのは,JavaScriptの浮動小数点数の精度によるものである.
そして,長方形の面積は24(4 * 6)となる.
総称型(ジェネリクス関数)
ジェネリクス関数とは
型パラメータを用いて複数の型に対応できる関数.同じロジックを様々な型に適用することが可能となる.
ジェネリクス関数の利点
- 型の柔軟性: 一つの関数定義で複数の型に対応できる.
- 型安全性: コンパイル時に型チェックが行われ,型の不整合を防ぐ.
- コードの再利用性: 同じロジックを異なる型に適用できるため,コードの重複を減らせる.
- 明確な型指定: 関数の使用時に型を明示的に指定できる.
function functionName<T>(parameter: T): T {
// 関数の本体
return parameter;
}
ここで,Tは型パラメータであり,関数の呼び出し時に具体的な型が指定される
例1: スワップ関数
任意の型の2つの値を交換するジェネリック関数を実装する.
function swap<T>(a: T, b: T): [T, T] {
return [b, a];
}
// 使用例
let x = 5, y = 10;
[x, y] = swap(x, y);
console.log(x, y); // 出力: 10 5
let str1 = "hello", str2 = "world";
[str1, str2] = swap(str1, str2);
console.log(str1, str2); // 出力: world hello
この関数は,任意の型Tの2つの値を受け取り,それらを交換して返す.数値,文字列,オブジェクトなど,さまざまな型に対して使用できる.
例2: 最小値を求める関数
配列の中から最小値を見つけるジェネリック関数を実装する.
function findMin<T>(arr: T[], compareFn: (a: T, b: T) => number): T | undefined {
if (arr.length === 0) return undefined;
return arr.reduce((min, current) => compareFn(min, current) <= 0 ? min : current);
}
// 使用例
const numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
console.log(findMin(numbers, (a, b) => a - b)); // 出力: 1
const strings = ["apple", "banana", "cherry", "date"];
console.log(findMin(strings, (a, b) => a.localeCompare(b))); // 出力: "apple"
この関数は,任意の型Tの配列と比較関数を受け取り,最小値を返す.比較関数を引数として受け取ることで,数値や文字列だけでなく,複雑なオブジェクトの比較にも対応できる.
例3: キーと値のペアを管理するクラス
キーと値のペアを管理するジェネリッククラスを実装する.
class KeyValuePair<K, V> {
constructor(public key: K, public value: V) {}
toString(): string {
return `${this.key}: ${this.value}`;
}
}
// 使用例
const pair1 = new KeyValuePair<number, string>(1, "one");
console.log(pair1.toString()); // 出力: 1: one
const pair2 = new KeyValuePair<string, boolean>("isActive", true);
console.log(pair2.toString()); // 出力: isActive: true
クラスに関してはあとで紹介する
このクラスは,キーの型Kと値の型Vを指定することで,さまざまな型の組み合わせに対応できる.文字列キーと数値値,数値キーとオブジェクト値など,多様な使用シーンに適用できる.
クロージャー
クロージャーとは
関数とその関数が宣言されたレキシカルスコープの組み合わせである.
関数が自身の外部で定義された変数にアクセスできる仕組みのことである.
クロージャーの主な特徴
- 関数のスコープ保持: 関数が定義された環境の変数にアクセスできる.
- データの隠蔽: 外部から直接アクセスできない変数を保持できる.
- 状態の保持: 関数が呼び出されるたびに状態を維持できる.
- 柔軟な関数生成: 動的に関数を生成し,環境に応じた振る舞いを実現できる.
クロージャーを使う場面
- プライベート変数の実現
- コールバック関数での状態保持
- 関数型プログラミングにおける部分適用や関数合成
- モジュールパターンの実装
例1: カウンター関数
プライベートな状態を持つカウンター関数を実装する.
function createCounter(initialValue: number = 0) {
let count = initialValue;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};
}
// 使用例
const counter = createCounter(5);
console.log(counter.getCount()); // 出力: 5
console.log(counter.increment()); // 出力: 6
console.log(counter.decrement()); // 出力: 5
この関数は,初期値を受け取り,カウントを増減させるメソッドと現在の値を取得するメソッドを持つオブジェクトを返す.カウントの状態はクロージャー内に保持され,外部からは直接アクセスできない.
例2: メモ化関数
関数の結果をキャッシュし,同じ引数で呼び出された場合に計算を省略する関数を実装する.
function memoize<T>(fn: (...args: any[]) => T) {
const cache = new Map<string, T>();
return (...args: any[]): T => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key)!;
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
// 使用例
const expensiveCalculation = (n: number): number => {
console.log("Calculating...");
return n * 2;
};
const memoizedCalculation = memoize(expensiveCalculation);
console.log(memoizedCalculation(5)); // 出力: Calculating... 10
console.log(memoizedCalculation(5)); // 出力: 10 (キャッシュから)
この関数は,任意の関数を受け取り,その結果をキャッシュする新しい関数を返す.クロージャーを使用してキャッシュをプライベートに保持し,同じ引数で呼び出された場合に計算を省略する.
例3: 設定可能な遅延実行関数
指定した時間後に関数を実行する,設定可能な遅延実行関数を実装する.
function createDelayedExecutor(defaultDelay: number) {
return function<T>(fn: () => T, delay: number = defaultDelay): Promise<T> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(fn());
}, delay);
});
};
}
// 使用例
const delayExecution = createDelayedExecutor(1000);
delayExecution(() => console.log("Hello"), 2000);
delayExecution(() => console.log("World"));
この関数は,デフォルトの遅延時間を受け取り,指定した関数を遅延実行する新しい関数を返す.クロージャーを使用してデフォルトの遅延時間を保持し,必要に応じて上書きできるようにする.
関数まとめ Part2
関数のオプション引数と既定値,関数のオーバーロード,ジェネリクス関数,クロージャー
-
関数のオプション引数と既定値は,引数の省略や既定値の設定を可能にする機能である.これにより,柔軟な関数設計が可能となり,様々な使用シーンに対応できる.
-
関数のオーバーロードは,同じ名前の関数を異なる引数で複数定義できる機能である.これにより,引数の型や数に応じて適切な処理を行うことができ,コードの可読性と再利用性が向上する.
-
ジェネリクス関数は,型パラメータを用いて複数の型に対応できる関数である.これにより,型の柔軟性を保ちつつ,型安全性も確保することができる.さまざまな型に対して同じロジックを適用する場合に特に有用である.
-
クロージャーは,関数が自身の外部で定義された変数にアクセスできる仕組みである.これにより,データの隠蔽や状態の保持が可能となり,より高度な関数型プログラミングが実現できる.
オブジェクト指向プログラミング(OOP)とは
そもそもプログラミングとは
本質的には「さまざまな物事を記述すること」.
その目的は効率化やシステム開発など多岐にわたるが, 根本的には現実世界や仮想世界をどのように表現するかということである
従来のプログラミング手法では, 複雑な現実世界を表現する際に制限があった. そこで登場したのが「オブジェクト指向プログラミング」という概念である.
本記事ではオブジェクト指向プログラミングはなんとなく知っている人やよく知っている人向けに説明はかなり簡単にして,文法重視で記載しています.オブジェクト指向の概念はこちらの記事でかなり詳しく説明しています.
オブジェクト指向初めての人は一読することをお勧めします.手続型プログラミングからの変遷について詳しく書きました
オブジェクト指向プログラミングの主な特徴
-
オブジェクト:現実世界の「もの」や「概念」を表現する
- オブジェクトは, データ(属性)と振る舞い(メソッド)を持つ.
- 例えば, 「車」というオブジェクトは, 色や型番といった属性と, 走る・止まるといった振る舞いを持つ.
-
クラス:オブジェクトの設計図
- クラスは, オブジェクトの構造と振る舞いを定義する.
- クラスを基に, 具体的なオブジェクト(インスタンス)を生成する.
-
カプセル化:データとその操作をひとまとめにする
- オブジェクトの内部データを外部から直接操作できないようにし, メソッドを通じてアクセスする.
- これにより, データの整合性を保ち, プログラムの安全性を高める.
-
継承:既存のクラスを基に新しいクラスを作成する
- 既存のクラス(親クラス)の特性を引き継ぎ, 新しいクラス(子クラス)を作る.
- コードの再利用性を高め, 階層構造を表現できる.
-
ポリモーフィズム:同じインターフェースで異なる動作を実現する
- 同じメソッド名で異なる処理を実行できる.
- これにより, コードの柔軟性と拡張性が向上する.
クラス
クラスとは
オブジェクト指向プログラミングの基本的な概念である.クラスを使うことで,データと振る舞いをカプセル化し,再利用可能なコードを作成できる.
クラスの定義
class Animal {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
このクラスの構造は,クラス名として "Animal"という名前でクラスを定義して,nameとageという2つのプロパティを持つ.これらはクラスのデータを表す.constructorメソッドは,クラスのインスタンスを作成する際に呼び出される.ここで,nameとageを初期化している.
コンストラクタは以下で説明する
クラスの使用例
let cat = new Animal("Whiskers", 3);
console.log(cat.name); // "Whiskers"を出力
console.log(cat.age); // 3を出力
インスタンスの作成にはnewを使う.「.」で区切ってメンバーを書けば、クラスのメンバーが利用できる.
コンストラクターの定義と使用
クラスにおいて, コンストラクターは特別なメソッドであり, オブジェクトの初期化に使用される.
class Animal {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
-
constructor
というキーワードを使用してコンストラクターを定義している. -
コンストラクターは2つのパラメータ (
name
とage
) を受け取る. -
コンストラクター内で, 受け取ったパラメータを使ってクラスのプロパティを初期化している.
以下に, このクラスの使用例を示す.
const dog = new Animal("ポチ", 3);
console.log(dog.name); // 出力: ポチ
console.log(dog.age); // 出力: 3
const cat = new Animal("タマ", 5);
console.log(cat.name); // 出力: タマ
console.log(cat.age); // 出力: 5
この例では, Animal
クラスのインスタンスを作成する際に, コンストラクターを通じて名前と年齢を設定している. コンストラクターを使用することで, オブジェクトの作成と同時に必要な初期化を行うことができ, コードの簡潔さと安全性を向上させることができる.
クラスのメソッド定義とオーバーロード
クラスにメソッドを追加することで, オブジェクトの振る舞いを定義できる.
class Book {
title: string;
author: string;
pages: number;
constructor(title: string, author: string, pages: number) {
this.title = title;
this.author = author;
this.pages = pages;
}
// 基本的なメソッド定義
getInfo(): string {
return `${this.title} by ${this.author}, ${this.pages} pages`;
}
// メソッドのオーバーロード
read(): void;
read(pages: number): void;
read(pages?: number): void {
if (typeof pages === "number") {
console.log(`Reading ${pages} pages of ${this.title}`);
} else {
console.log(`Reading entire book: ${this.title}`);
}
}
}
-
getInfo
メソッドは, 本の情報を文字列として返す基本的なメソッドである. -
read
メソッドは, オーバーロードされている. 引数なしで呼び出すと本全体を読むことを, 引数ありで呼び出すと指定ページ数を読むことを表現している.
以下に, このクラスの使用例を示す.
const myBook = new Book("TypeScript入門", "山田太郎", 300);
console.log(myBook.getInfo());
// 出力: TypeScript入門 by 山田太郎, 300 pages
myBook.read();
// 出力: Reading entire book: TypeScript入門
myBook.read(50);
// 出力: Reading 50 pages of TypeScript入門
このように, メソッドを定義することで, オブジェクトの振る舞いを表現できる. また, オーバーロードを使用することで, 同じメソッド名で異なる引数パターンに対応できる.
クラスでのアクセス修飾子
TypeScriptでは,クラスのプロパティやメソッドにアクセス修飾子を使用できる.主な修飾子は以下の3つ:
public: デフォルトの修飾子。クラス外からアクセス可能
private: クラス内からのみアクセス可能
protected: クラス内および派生クラスからアクセス可能
class Person {
public name: string;
private age: number;
protected id: number;
constructor(name: string, age: number, id: number) {
this.name = name;
this.age = age;
this.id = id;
}
public introduce(): string {
return `My name is ${this.name}`;
}
private getAge(): number {
return this.age;
}
protected getId(): number {
return this.id;
}
}
ゲッターとセッター
プロパティへのアクセスと変更をより細かく制御するために、ゲッターとセッターを使用する.同じメソッド名で値へのアクセス用と設定用の2つを定義できる.
class Circle {
private _radius: number;
constructor(radius: number) {
this._radius = radius;
}
get radius(): number {
return this._radius;
}
set radius(value: number) {
if (value <= 0) {
throw new Error("Radius must be positive");
}
this._radius = value;
}
get area(): number {
return Math.PI * this._radius ** 2;
}
}
const circle = new Circle(5);
console.log(circle.radius); // 5
circle.radius = 10;
console.log(circle.area); // 約314.16
静的メンバー
クラスのインスタンスではなく、クラス自体に属するメンバーを定義するには、static キーワードを使用する.
class MathOperations {
static PI: number = 3.14159;
static add(a: number, b: number): number {
return a + b;
}
static multiply(a: number, b: number): number {
return a * b;
}
}
console.log(MathOperations.PI); // 3.14159
console.log(MathOperations.add(5, 3)); // 8
console.log(MathOperations.multiply(4, 2)); // 8
情報の隠蔽の実装
class BankAccount {
private accountNumber: string;
private balance: number;
constructor(accountNumber: string, initialBalance: number) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
public getAccountNumber(): string {
return "****" + this.accountNumber.slice(-4);
}
public getBalance(): number {
return this.balance;
}
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
} else {
console.log("入金額は正の数でなければなりません.");
}
}
public withdraw(amount: number): boolean {
if (amount > 0 && amount <= this.balance) {
this.balance -= amount;
return true;
} else {
console.log("引き出しに失敗しました. 残高が不足しているか, 引き出し額が不正です.");
return false;
}
}
}
-
accountNumber
とbalance
は private 変数として宣言されている. これにより, クラス外からの直接アクセスが防がれる. -
getAccountNumber
メソッドは, アカウント番号の最後の4桁のみを表示し, セキュリティを確保している. -
deposit
メソッドは, 入金額が正の数であることを確認してから残高を更新する. -
withdraw
メソッドは, 引き出し額が正の数で, かつ残高以下であることを確認してから処理を行う.
以下に, このクラスの使用例を示す.
const account = new BankAccount("1234567890", 1000);
console.log(account.getAccountNumber()); // 出力: ****7890
console.log(account.getBalance()); // 出力: 1000
account.deposit(500);
console.log(account.getBalance()); // 出力: 1500
account.withdraw(200);
console.log(account.getBalance()); // 出力: 1300
account.withdraw(2000); // 出力: 引き出しに失敗しました. 残高が不足しているか, 引き出し額が不正です.
console.log(account.getBalance()); // 出力: 1300
この例では, BankAccount
クラスのインスタンスを通じて銀行口座の操作を行っている. 重要な情報である口座番号と残高は直接アクセスできないが, 適切なメソッドを通じて安全に操作できる. これにより, データの整合性とセキュリティが確保される.
情報の隠蔽を適切に実装することで, オブジェクトの内部状態を保護し, 予期せぬ変更や不正なアクセスを防ぐことができる. また, クラスの内部実装を変更する際も, 外部のコードに影響を与えることなく柔軟に対応できる.
クラスの継承とメソッドのオーバーライド
class Vehicle {
protected make: string;
protected model: string;
protected year: number;
constructor(make: string, model: string, year: number) {
this.make = make;
this.model = model;
this.year = year;
}
public getInfo(): string {
return `${this.year} ${this.make} ${this.model}`;
}
public start(): string {
return "エンジンを始動します";
}
}
class Car extends Vehicle {
private numDoors: number;
constructor(make: string, model: string, year: number, numDoors: number) {
super(make, model, year);
this.numDoors = numDoors;
}
public start(): string {
return `${super.start()} キーを回します`;
}
public drive(): string {
return "車を運転します";
}
}
class ElectricCar extends Car {
private batteryCapacity: number;
constructor(make: string, model: string, year: number, numDoors: number, batteryCapacity: number) {
super(make, model, year, numDoors);
this.batteryCapacity = batteryCapacity;
}
public start(): string {
return "電源ボタンを押します";
}
public charge(): string {
return "バッテリーを充電します";
}
}
let myCar = new Car("Toyota", "Corolla", 2022, 4);
let myTesla = new ElectricCar("Tesla", "Model 3", 2023, 4, 75);
console.log(myCar.getInfo());// 2022 Toyota Corolla
console.log(myCar.start());// エンジンを始動します キーを回します
console.log(myCar.drive());// 車を運転します
console.log(myTesla.getInfo());// 2023 Tesla Model 3
console.log(myTesla.start());// 電源ボタンを押します
console.log(myTesla.charge());// バッテリーを充電します
このコード例では、Vehicle(乗り物)を基底クラスとし、Car(車)をその派生クラス、さらにElectricCar(電気自動車)をCarの派生クラスとして定義している。これにより、クラスの継承の階層構造を示している。
-
継承の仕組み:
Carクラスは「extends Vehicle」によってVehicleクラスを継承している。
ElectricCarクラスは「extends Car」によってCarクラスを継承している。
継承により、派生クラスは基底クラスのプロパティとメソッドを受け継ぐ。 -
コンストラクタとsuper:
派生クラスのコンストラクタでは、superキーワードを使用して親クラスのコンストラクタを呼び出している。
これにより、親クラスで定義されたプロパティを適切に初期化できる。 -
メソッドのオーバーライド:
Carクラスでは、Vehicleクラスのstartメソッドをオーバーライドしている。
オーバーライドされたメソッドでは、super.start()を呼び出すことで親クラスの処理も実行している。
ElectricCarクラスでは、startメソッドを完全に置き換えている。 -
独自のメソッドとプロパティ:
CarクラスにはnumDoorsプロパティとdriveメソッドがある。
ElectricCarクラスにはbatteryCapacityプロパティとchargeメソッドがある。
これらは各クラス特有の属性や機能を表現している。 -
多態性:
ElectricCarのインスタンスは、ElectricCar、Car、Vehicleのいずれの型としても扱える。
これにより、同じメソッド名(例:start)でも、実際のオブジェクトの型に応じて適切な処理が実行される。
抽象クラス
抽象クラスは、他のクラスが継承するためのベースクラスとして使用される.抽象クラス自体はインスタンス化できない.
abstract class Shape {
abstract getArea(): number;
printArea(): void {
console.log(`Area: ${this.getArea()}`);
}
}
class Square extends Shape {
constructor(private side: number) {
super();
}
getArea(): number {
return this.side ** 2;
}
}
const square = new Square(5);
square.printArea(); // Area: 25
関数,インターフェース,クラスの違い
function (関数):
関数は、特定のタスクを実行するためのコードブロックである。入力を受け取り、処理を行い、結果を返す。
function add(a: number, b: number): number {
return a + b;
}
console.log(add(3, 5)); // 出力: 8
interface (インターフェース):
インターフェースは、オブジェクトの構造を定義するためのものである。メソッドやプロパティの型を指定するが、実装は提供しない。
interface Shape {
area(): number;
perimeter(): number;
}
class Circle implements Shape {
constructor(private radius: number) {}
area(): number {
return Math.PI * this.radius ** 2;
}
perimeter(): number {
return 2 * Math.PI * this.radius;
}
}
const circle = new Circle(5);
console.log(circle.area()); // 出力: 78.53981633974483
console.log(circle.perimeter()); // 出力: 31.41592653589793
class (クラス):
クラスは、オブジェクト指向プログラミングの中心的な概念である。データ(プロパティ)と振る舞い(メソッド)を一つの単位にカプセル化する。継承やポリモーフィズムなどの機能を提供する。
class Animal {
constructor(protected name: string) {}
makeSound(): string {
return "Some generic animal sound";
}
}
class Dog extends Animal {
constructor(name: string, private breed: string) {
super(name);
}
makeSound(): string {
return "Woof!";
}
getInfo(): string {
return `${this.name} is a ${this.breed}`;
}
}
const myDog = new Dog("Buddy", "Golden Retriever");
console.log(myDog.makeSound()); // 出力: Woof!
console.log(myDog.getInfo()); // 出力: Buddy is a Golden Retriever
Node.jsとはなんなのか
では,Node.jsとはなんなのか
JSのみではブラウザ上でしか動かない仕様上どうしても使いにくい欠点があった.また,せっかくWebアプリを作るのならばブラウザで動く機能とOSで動く機能両方を1つの言語でかけるとすごく便利ですよね?
これを解決してくれたのがNode.jsである.
Node.jsとはよくサーバーサイドのJSと言われるがちょっと違って,先ほど述べた通り,ブラウザ&サーバーの機能を持つため,サーバーサイドのJSとは限らない.
そのため,
Node.jsはWikipediaにも書いてある通り,Webフレームワークでもなく,フロントエンドのライブラリでもない,JavaScript実行環境である.
つまり,これでPythonやJavaのようにwebアプリが作れたり,APIが作れたりする.
ここで
じゃあReact,Vue.js,next.jsって...つまり...
と察した方,その通りです.
Node.jsは、React、Vue.js、Next.jsなどのフレームワークやライブラリと密接に関わっている.
ですが...それぞれの関係性を紹介する前にまずNode.jsと繋ぐためのものnpm
について紹介する.
npmってナンダ?
npm(Node Package Manager)は,Node.jsのパッケージマネージャーのこと.
パッケージマネージャー
ソフトウェアの開発に必要なライブラリ,フレームワーク,ツールなどのパッケージを管理するためのツール.
npmは以下のような役割がある.
パッケージの管理
プロジェクトで使用するパッケージ(ライブラリ)を簡単にインストール,更新,削除できる.
パッケージの依存関係も自動的に管理されるため,手動でパッケージをダウンロードしたり,依存関係を解決したりする必要がない.
パッケージの共有
npmは,オープンソースのパッケージレジストリを提供している.開発者は自身のパッケージを公開することで,他の開発者と共有できる.他の開発者が公開したパッケージを使うことで,開発の効率が大幅に向上する.
スクリプトの実行
npmを使って,プロジェクトに関連するスクリプト(コマンド)を定義し,実行できる.
例えば、開発サーバーの起動、テストの実行、ビルドの実行などのスクリプトを package.json
ファイルに定義し,以下のnpm run コマンドで実行できる.
npm run
バージョン管理
npmは,パッケージのバージョン管理を行う.これにより,プロジェクトで使用するパッケージのバージョンを固定できる.
バージョンを固定することで、,パッケージの更新によって意図しない動作変更が起きることを防げる.
つまり,npmはNode.jsの開発において欠かせないツールであり,Node.jsの中心的な存在であると言える.React、Vue.js、Next.jsなどのフロントエンド開発でも,npmは広く使われている.
フレームワークとライブラリの違い
さらにNode.jsを知るためにはフレームワークとライブラリの違いを知っておく必要がある.
フレームワーク
は,アプリケーションの全体的な流れや構造を制御する.開発者は,フレームワークが提供する規約やルールに従ってコードを書くことになる. フレームワークは,開発者が書いたコードを呼び出す形で動作する.
ライブラリ
は,特定の機能を提供するツールである.アプリケーションの流れや構造は開発者が決定し,必要な時にライブラリの機能を呼び出して使う.
フレームワークとライブラリの区別は絶対的ではないと思う.例えば,Reactはライブラリと呼ばれることが多いが,Reactを中心にアプリケーションを構築する場合,Reactがアプリケーションの構造を大きく規定することになるため,フレームワークに近い性質を持つと言えるのではないかと思う.
つまり絵で表すとこのようであると言える.
Webアプリを作る際よくこの2つをみないだろうか?
Node.jsとReact,Vue.js,next.jsの関係性
これでやっと Node.jsとReact,Vue.js,next.jsの関係性について話せるので1つずつ説明する.
Node.js と React の関係
React はフロントエンドのJavaScriptライブラリで,UIの構築に使われる.
Reactの開発環境を構築する際に,まず,Node.jsが使われる.
例えば、Create React App (CRA) というReactプロジェクトの雛形を作成するツールは,Node.js上で動作する.
また、ReactコンポーネントをJSXで記述する際,BabelというトランスパイラがJSXをJavaScriptに変換するが,このBabelもNode.js上で動作する.
つまり,ReactはNode.jsが不可欠であり,実行環境としてガッツリ使っている.
Node.js と Vue.js の関係
Vue.jsは,フロントエンドのJavaScriptフレームワークである.
Vue.jsの開発環境の構築にもNode.jsが使われる.
例えば,Vue CLI というVue.jsプロジェクトの雛形を作成するツールは,Node.js上で動作する.
また,Vue.jsでもBabelが使われることがあり,その場合はNode.js上で動作する.
つまり,Vue.jsもガッツリNode.jsで実行している.
Node.js と Next.js の関係
Next.jsは,ReactをベースにしたフレームワークでSSR(サーバーサイドレンダリング)を実現できる.
Next.jsも,Node.js上で動作する.
つまり,Next.jsもガッツリNode.jsが必須である.
Next.jsを使ってSSRを行う際、サーバーサイドの処理はNode.jsで行われる.
ReactとVue.jsの違い
React
は、UIを構築するためのJavaScriptライブラリである.コンポーネントベースのアプローチで,仮想DOMを使って効率的にDOMを更新する.Reactはビューに特化したライブラリで,状態管理やルーティングなどの機能は別のライブラリ(Redux, React Router)を使って実現する.
Vue.js
は,包括的なフレームワークと呼ばれることが多い.Vue.jsは,UIの構築に加えて,状態管理(Vuex)やルーティング(Vue Router)などの機能も公式ライブラリとして提供している.これらのライブラリはVue.jsと深く統合されており,シームレスに使うことができる.
混乱するかもしれないが...(私個人の意見としては)
Vue.jsのコアライブラリ自体はビューに特化しており,その意味ではライブラリと呼ぶこともできるだろう.また,Reactも,Reduxなどの状態管理ライブラリと組み合わせることで,フレームワークのような使い方ができる.
Node.jsで何ができるのか
ここでNode.jsってUIなどフロントエンドからサーバーサイドのバックエンドもできることがわかった.結局何ができるのかまとめてみた.
Webアプリ
例えばPythonでwebアプリを作るのであればDjango,Flaskなどがフレームワークとして挙げられる.
RubyならRuby on Railsというように,
Node.jsならNext.jsを使う,といった形でWebアプリを作れる.
Wikipediaにも書いてあるが,元々,Node.jsは大規模な同時接続を処理できるためのプラットフォームとして開発された.極端に言えばnginxやApacheのようなことも可能である.つまり,HTTPリクエストの受け取り,処理も可能だ.
モバイルApp
Node.jsはモバイルアプリケーションを作るためのフレームワークであるReactNativeがある. JS自体がReactのようにUI面において多くの機能を持っていることから,同じようにモバイルアプリに転用できる.また,JSで書かれていることもあり,Kotlin,JavaベースのAndroidアプリ開発やSwiftベースのiOSアプリ開発に手を出しづらい開発者にも人気である.Android,iOSの壁を感じることなく開発でき,クロスプラットフォームでのアプリケーションを作成できる.
ほとんどのネイティブ機能(Android,iOS独自の機能)はJavaScriptから使えるが,一部の高度な機能(例:ARKit、Android Fragment)を使うにはネイティブのコード(KotlinやSwift)を書く必要があ流.
デスクトップアプリ
Node.jsは他にもデスクトップアプリケーションを作るためのフレームワークであるElectronがある.ElectronもWindows,Mac,Linuxでのクロスプラットフォーム開発が可能である.
有名アプリだと,VSCode,Discord,Slackはこれで作られている.
コード検証ツール
Eslintといった静的コード検証ツールが使える.コードを実行せずにコードを解析し,潜在的なバグ,スタイルの不一致,アンチパターンなどを検出できる.
以下の記事で詳しく説明する
[鋭意製作中]
React VueでのTypeScriptの応用例
それではこのNode.jsを使ってフロントエンド開発をしてみよう.今回はReactまたはVue.jsを使って開発してみる.
どちらもコンポーネントベースアーキテクチャを採用している.
コンポーネントベースアーキテクチャとは
例えばWebアプリを作る際に1つのwebページごとに毎回 HTML/CSS/JS(TS)を書くのは勿体無い.
そもそもコンポーネントベースしか知らない人もいるかもしれないが,全部生のHTMLで各ページごとに全体タイトルからサブタイトル,本文まで同じものを毎度毎度作るということである.
これら全てページごとにテンプレートを書くのではなく,
共通コンポーネントとして
- 全体タイトル
- 広告
- サブタイトル(n)
を作成し,
sam1ページでは,
- サブタイトル(n=1)
- 本文1
を作成し,
sam2ページでは,
- サブタイトル(n=2)
- 本文2
このように作成する.
これにより記述する量がへり,簡単にWebページを作成できる.
まずはReactとVue.jsの書き方を見てコンポーネントベースアーキテクチャを見てみよう.
Reactではどのように書くのか
プロジェクトがあることを前提とする.以下のコマンドで作成する
npx create-react-app react-example
cd react-example
デフォルトでこうなっていると仮定
.
├── README.md
├── node_modules
│ (長いので省略)
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
└── src
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
├── reportWebVitals.js
└── setupTests.js
共通コンポーネントの作成
cd src
mkdir components
まず,共通コンポーネントとして
1.メインタイトルの作成
import React from 'react';
const GlobalTitle: React.FC = () => {
return <h1>全体タイトル</h1>;
}
export default GlobalTitle;
2.広告欄の作成
import React from 'react';
const Advertisement: React.FC = () => {
return <div style={{ backgroundColor: 'blue', padding: '20px', color: 'white' }}>広告</div>;
}
export default Advertisement;
3.サブタイトルの雛形作成
import React from 'react';
interface HeadingProps {
title: string;
}
const Heading: React.FC<HeadingProps> = ({ title }) => {
return <h2>{title}</h2>;
}
export default Heading;
各ページ固有のコンポーネントの作成
1./sam1ページの作成
import React from 'react';
import GlobalTitle from './GlobalTitle';
import Heading from './Heading';
import Advertisement from './Advertisement';
const Sam1Page: React.FC = () => {
return (
<div>
<GlobalTitle />
<Heading title="サブタイトル1" />
<div>本文1</div>
<Advertisement />
</div>
);
}
export default Sam1Page;
2./sam2ページの作成
import React from 'react';
import GlobalTitle from './GlobalTitle';
import Heading from './Heading';
import Advertisement from './Advertisement';
const Sam2Page: React.FC = () => {
return (
<div>
<GlobalTitle />
<Heading title="サブタイトル2" />
<div>本文2</div>
<Advertisement />
</div>
);
}
export default Sam2Page;
各ページのコンポーネントを指定
ここで各ページ(Sam1Page.tsx,Sam2Page.ts)のメインとなるコンポーネントを表すJSを指定する.
import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Sam1Page from './components/Sam1Page';
import Sam2Page from './components/Sam2Page';
const App: React.FC = () => {
return (
<Router>
<Routes>
<Route path="/sam1" element={<Sam1Page />} />
<Route path="/sam2" element={<Sam2Page />} />
</Routes>
</Router>
);
}
export default App;
このコマンドで実行する.
npm start
Vue.jsではどのように書くのか
Vue.jsのプロジェクトがあることを前提とする.
Vueのインストール
npm install -g @vue/cli
Vueプロジェクトの作成
vue create vue-example
cd vue-example
必要なパッケージのインストール
npm install vue-router@4
このような構造であることを前提とする
.
├── README.md
├── babel.config.js
├── jsconfig.json
├── node_modules
│(長いので割愛)
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── App.vue
│ ├── assets
│ ├── components
│ └── main.js
└── vue.config.js
共通コンポーネントの作成
1.メインタイトルの作成
<template>
<h1>全体タイトル</h1>
</template>
<script>
export default {
name: 'GlobalTitle'
}
</script>
2.広告欄の作成
<template>
<div>広告コンポーネント</div>
</template>
<script>
export default {
name: 'Advertisement'
}
</script>
3.サブタイトルの雛形作成
<template>
<h2>{{ title }}</h2>
</template>
<script>
export default {
name: 'Heading',
props: {
title: String
}
}
</script>
各ページ固有のコンポーネントの作成
1./sam1ページの作成
<template>
<div>
<GlobalTitle />
<Heading title="サブタイトル1" />
<div>本文1</div>
<Advertisement />
</div>
</template>
<script>
import GlobalTitle from '@/components/GlobalTitle.vue'
import Heading from '@/components/Heading.vue'
import Advertisement from '@/components/Advertisement.vue'
export default {
name: 'Sam1Page',
components: {
GlobalTitle,
Heading,
Advertisement
}
}
</script>
2./sam2ページの作成
<template>
<div>
<GlobalTitle />
<Heading title="サブタイトル2" />
<div>本文2</div>
<Advertisement />
</div>
</template>
<script>
import GlobalTitle from '@/components/GlobalTitle.vue'
import Heading from '@/components/Heading.vue'
import Advertisement from '@/components/Advertisement.vue'
export default {
name: 'Sam2Page',
components: {
GlobalTitle,
Heading,
Advertisement
}
}
</script>
各ページのコンポーネントを指定
ここで各ページ(Sam1Page.vue,Sam2Page.vue)のメインとなるコンポーネントを表すJSを指定する.
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
<template>
<router-view/>
</template>
<script>
export default {
name: 'App'
}
</script>
import { createRouter, createWebHistory } from 'vue-router'
import Sam1Page from '@/views/Sam1Page.vue'
import Sam2Page from '@/views/Sam2Page.vue'
const routes = [
{
path: '/sam1',
name: 'Sam1',
component: Sam1Page
},
{
path: '/sam2',
name: 'Sam2',
component: Sam2Page
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
このコマンドで実行する.
npm run serve
ReactやVue.jsからわかるコンポーネントの考え方
コンポーネントの考え方は,現代のフロントエンド開発において中核を成す概念である.ReactやVue.jsなどのフレームワークを通じて,この考え方の重要性と利点が明確になっている.私なりにどのようなことを目的に考えているのかいくつかあげてみた.
モジュール性と再利用性
コンポーネントは,ユーザーインターフェイス(UI)を独立した,再利用可能な部品に分割する方法である.これにより複雑なアプリケーションを管理しやすい小さな単位(コンポーネント)に分解できる.例えば,ボタン,フォーム,ナビゲーションバーなどのUI要素を個別のコンポーネントとして作成し,アプリケーション全体で繰り返し使用することができる.これにより,コードの重複を減らし,一貫性のあるUIを維持しやすくなる.
カプセル化
コンポーネントは,その機能に必要なすべての要素(マークアップ,ロジック,スタイル)を一つのユニットにカプセル化する.この特性により,コードの管理が容易になり,他の部分に影響を与えることなく個々のコンポーネントを変更したり改善したりできる.カプセル化は,コンポーネント間の依存関係を最小限に抑え,アプリケーションの保守性を高める.
データフローと状態管理
コンポーネントは,データの流れと状態管理を明確にする.親コンポーネントから子コンポーネントへのデータの受け渡し(プロップス)や,コンポーネント内部での状態管理の仕組みが整理される.この明確なデータフローにより,アプリケーション全体の状態変化を追跡しやすくなり,デバッグや機能拡張が容易になる.また,単方向データフローを採用することで,予測可能性が高まり,複雑なアプリケーションでも状態の一貫性を保ちやすくなる.
単一責任の原則
各コンポーネントは,特定の機能や表示に責任を持つべきである.これは,ソフトウェア設計の「単一責任の原則」に沿っている.コンポーネントが一つの明確な役割を持つことで,コードの保守性と拡張性が向上する.例えば,ユーザー認証を扱うコンポーネントは認証のロジックのみを含み,UIの表示は別のコンポーネントに任せるといった具合である.このアプローチにより,各コンポーネントの機能が明確になり,必要に応じて容易に修正や置換が可能になる.
階層構造とコンポジション
コンポーネントは階層構造を形成し,より小さなコンポーネントを組み合わせてより大きなコンポーネントを作成できる.この「コンポジション」により,複雑なUIを構築する際の柔軟性が高まる.例えば,「ボタン」コンポーネントと「入力フィールド」コンポーネントを組み合わせて「検索バー」コンポーネントを作成し,さらにそれを「ヘッダー」コンポーネントの一部として使用するといった具合である.この階層構造により,UIの構成を論理的に整理でき,開発者間でのコードの理解と共有が簡単になる.
テスタビリティ
個々のコンポーネントは独立しているため,単体テストが容易になる.各コンポーネントに対して専用のテストを書くことで,アプリケーション全体の品質と信頼性を向上させることができる.また,コンポーネントのプロップスや状態を操作してさまざまな条件下でのテストが可能になり,エッジケースの検出も容易になる.さらに,モックやスタブを使用して,コンポーネントの依存関係を分離したテストも実施しやすくなる.
パフォーマンス最適化
コンポーネントベースのアプローチでは,必要な部分だけを更新することが可能になる.これにより,アプリケーション全体のパフォーマンスを向上させることができる.例えば,大規模なリストの一部だけが変更された場合,そのリスト全体ではなく変更されたアイテムのコンポーネントのみを再描画することができる.また,コンポーネントの純粋性(同じ入力に対して常に同じ出力を返す性質)を保つことで,不要な再描画を防ぎ,メモ化などの最適化技術も適用しやすくなる.
チーム開発の効率化
コンポーネントベースの開発により,チームメンバーが並行して異なるコンポーネントを開発することができる.これにより,開発プロセスが効率化され,大規模プロジェクトの管理が容易になる.各開発者が担当するコンポーネントの責任範囲が明確になるため,コードの競合も減少する.また,コンポーネントライブラリを作成することで,チーム内での知識の共有や再利用可能なコードの蓄積が促進される.
ReactやVue.jsでは以上のメリットがあってコンポーネントベースがよく使われるのかなと思う.ただし,コンポーネントベースが全ていいわけではなく,デメリットも存在することもある.(例えば学習コストの高さなど)しかし開発者,ユーザーの目線では使いやすい開発手法なのかなとは思うのでぜひ使ってみてほしい.
UIライブラリ
UIライブラリとは
ユーザーインターフェース(UI)の構築を効率化するために設計された,再利用可能なコンポーネントとツールの集合体である.開発者がアプリケーションの見た目と使い勝手を迅速に構築できるようにし,一貫性のあるデザインの実現と開発時間の短縮をすることができる.
UIライブラリの特徴
- コンポーネント: ボタン,フォーム,ナビゲーションバーなどの再利用可能なUI要素を提供する.
- レスポンシブデザイン 様々な画面サイズやデバイスに対応するレイアウトシステムを含む.
- カスタマイズ機能: テーマやスタイリングのオプションを提供し,ブランドに合わせた調整を可能にする.
- アクセシビリティ: WAI-ARIAなどの標準に準拠し,障害を持つユーザーにも使いやすいインターフェースを作成できる.
- ドキュメンテーション: 使用方法や実装例を詳細に説明した文書を提供する.
本記事ではUIライブラリとしてMaterial-UIを使用する.Material-UIは,上記の特徴を備えたReactベースのUIライブラリの一つである.
Material-UIとは
ReactアプリケーションのためのUIコンポーネントライブラリである.Google's Material Designガイドラインに基づいて設計されており,美しく機能的なユーザーインターフェースを簡単に作成できる.
使用方法
import { <UIコンポーネント名> } from '@mui/material';
このようにコンポーネントファイルでインポートすると使える.
MUIの主要なUIコンポーネント
1. Button:
import React from 'react';
import { Button } from '@mui/material';
const ButtonExample: React.FC = () => {
return (
<>
<Button variant="contained">Contained</Button>
<Button variant="outlined">Outlined</Button>
<Button variant="text">Text</Button>
</>
);
};
export default ButtonExample;
Buttonコンポーネントは、クリック可能なボタンを作成する。variant プロパティでスタイルを指定でき、"contained"(塗りつぶし)、"outlined"(枠線のみ)、"text"(テキストのみ)がある。color プロパティで色を指定できる。
2.TextField
import React, { useState } from 'react';
import { TextField } from '@mui/material';
const TextFieldExample: React.FC = () => {
const [value, setValue] = useState('');
return (
<TextField
label="Name"
variant="outlined"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
};
export default TextFieldExample;
TextFieldは、テキスト入力フィールドを作成する。label プロパティでラベルを、variant プロパティでスタイルを指定できる。type プロパティで入力タイプ(password, email など)を指定できる。
3.AppBar と Toolbar コンポーネント
import React from 'react';
import { AppBar, Toolbar, Typography, Button } from '@mui/material';
const AppBarExample: React.FC = () => {
return (
<AppBar position="static">
<Toolbar>
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
My App
</Typography>
<Button color="inherit">Login</Button>
</Toolbar>
</AppBar>
);
};
export default AppBarExample;
AppBarは、アプリケーションのトップバーを作成する。Toolbarと組み合わせて使用し、その中にナビゲーション要素を配置する。position プロパティで固定位置を指定できる。
4.Card, CardContent, CardActions コンポーネント
import React from 'react';
import { Card, CardContent, CardActions, Typography, Button } from '@mui/material';
const CardExample: React.FC = () => {
return (
<Card sx={{ maxWidth: 345 }}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Card Title
</Typography>
<Typography variant="body2" color="text.secondary">
This is the card content. You can put any information here.
</Typography>
</CardContent>
<CardActions>
<Button size="small">Learn More</Button>
</CardActions>
</Card>
);
};
export default CardExample;
Cardは、情報をグループ化して表示するためのコンテナ。CardContentにメインコンテンツを、CardActionsにアクションボタンなどを配置する。
5.List, ListItem, ListItemText コンポーネント
import React from 'react';
import { List, ListItem, ListItemText, Divider } from '@mui/material';
const ListExample: React.FC = () => {
return (
<List>
<ListItem>
<ListItemText primary="Item 1" secondary="Description 1" />
</ListItem>
<Divider />
<ListItem>
<ListItemText primary="Item 2" secondary="Description 2" />
</ListItem>
</List>
);
};
export default ListExample;
Listは、項目を縦に並べて表示する。ListItemで各項目を、ListItemTextで項目のテキストを指定する。Dividerで項目間に区切り線を追加できる。
6.Dialog コンポーネント
import React, { useState } from 'react';
import { Button, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material';
const DialogExample: React.FC = () => {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={() => setOpen(true)}>Open Dialog</Button>
<Dialog open={open} onClose={() => setOpen(false)}>
<DialogTitle>Dialog Title</DialogTitle>
<DialogContent>
This is the dialog content. You can put any information here.
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)}>Close</Button>
</DialogActions>
</Dialog>
</>
);
};
export default DialogExample;
Dialogは、モーダルダイアログを作成する。DialogTitle、DialogContent、DialogActionsを使用して、ダイアログの各部分を構成する。open プロパティでダイアログの表示・非表示を制御する。
他の類似サービスの紹介
1. Ant Design:
中国のAlibabaグループが開発した,エンタープライズ向けのUIライブラリである.
特徴: 豊富なコンポーネント,綿密に設計されたUI,国際化サポート.
2. React Bootstrap:
BootstrapのReact実装である.
特徴: 親しみやすいデザイン,豊富なドキュメント,Bootstrapとの互換性.
3. Chakra UI:
アクセシビリティに焦点を当てたモダンなUIライブラリである.
特徴: 優れたアクセシビリティ,柔軟なスタイリング,ダークモードサポート.
4. Semantic UI React:
人間親和性の高いUIを目指したライブラリである.
特徴: 直感的な命名規則,豊富なテーマ,jQuery非依存.
5. Tailwind CSS:
ユーティリティファーストのCSSフレームワークである.
特徴: 高度なカスタマイズ性,小さなバンドルサイズ,迅速な開発.
APIをNode.js(TypeScript)で作成
バックエンドとはなんなのか
バックエンドとは
ソフトウェアシステムの中で,ユーザーの目に直接触れない部分を担当する領域である.
1. データの処理と保存:
ユーザーが入力したデータや,システムが生成したデータを処理し,データベースに保存する.また,必要に応じてデータを取り出し,加工する.
2. ビジネスロジックの実装:
アプリケーションの中核となる機能や規則を実装する.例えば,ユーザー認証,決済処理,データ分析などがこれに該当する.
3. セキュリティの確保:
不正アクセスやデータ漏洩を防ぐためのセキュリティ対策を実装する.
4. スケーラビリティの確保:
システムの負荷に応じて,処理能力を調整できるようにする.
バックエンドで使う名称
サーバー
物理的なハードウェアまたはクラウド上の仮想マシンで,バックエンドのコードを実行する.
Apache,Nginx,IISなどのWebサーバーソフトウェアを使用することが多い.
アプリケーションロジック
プログラミング言語(Java,Python,Ruby,Node.jsなど)で記述された,システムの主要な機能を実装するコード.
フレームワーク(Spring,Django,Ruby on Rails,Express.jsなど)を使用して開発効率を高めることが一般的.
データベース
構造化されたデータを保存,取得,管理するためのシステム.
関係データベース(MySQL,PostgreSQL,Oracle)やNoSQLデータベース(MongoDB,Cassandra)がよく使用される.
キャッシュ
頻繁にアクセスされるデータを一時的に保存し,応答時間を短縮するためのメモリ領域.
Redisやmemcachedなどのキャッシュシステムがよく利用される.
ジョブキュー
非同期処理や時間のかかるタスクを管理するためのシステム.
RabbitMQ,Apache Kafkaなどが使用される.
APIとはなんなのか
API(Application Programming Interface)とは
ソフトウェアコンポーネント間のインターフェースを定義したものである.バックエンドとフロントエンド(ユーザーインターフェース)を繋ぐ役割を果たす.
1. 標準化されたデータ交換:
通常,JSON or XMLなどの形式でデータをやり取りする.
2. エンドポイントの提供:
特定の機能やリソースにアクセスするためのURLを提供する.
3. HTTP(S)メソッドの利用:
GET,POST,PUT,DELETEなどのメソッドを使用してリソースの操作を行う.
4. 認証と認可:
APIキーやOAuthなどの仕組みを用いて,アクセス制御を行う.
APIの種類
RESTful API
現在最も一般的なAPI設計パターン.
リソースベースの設計で,HTTPメソッドを使用してCRUD操作(Create, Read, Update, Delete)を行う.
ステートレスな通信を基本とし,スケーラビリティに優れている.
GraphQL API
クライアントが必要なデータを柔軟に指定できるAPI.
オーバーフェッチングやアンダーフェッチングの問題を解決し,効率的なデータ取得が可能.
SOAP API
XMLベースのプロトコルを使用する従来型のAPI.
堅牢性と信頼性が高いが,RESTに比べて複雑で重い.
WebSocket API
リアルタイム双方向通信を実現するAPI.
チャットアプリケーションやライブ更新機能などに適している.
APIセキュリティ
- API キー:シンプルな認証方式.
- OAuth 2.0:トークンベースの認可フレームワーク.
- JWT(JSON Web Token):セキュアな情報伝送のための規格.
バックエンドとフロントエンドを繋ぐAPIの処理の流れ
ソフトウェア全体の観点から見ると,バックエンドとAPIは以下のように位置づけられる.
-
フロントエンド(ユーザーインターフェース)がユーザーからの入力を受け取る.
-
フロントエンドはAPIを通じてバックエンドにリクエストを送信する.
-
バックエンドはリクエストを処理し,必要に応じてデータベースとのやり取りを行う.
-
バックエンドは処理結果をAPIを通じてフロントエンドに返す.
-
フロントエンドは受け取ったデータを適切に表示し,ユーザーに結果を提示する.
このように,バックエンドとAPIは,ソフトウェアシステム全体の中で,データ処理とコンポーネント間の通信を担う重要な役割を果たしている.
バックエンドの設計(クリーンアーキテクチャとドメイン駆動設計(DDD))
クリーンアーキテクチャとは
クリーンアーキテクチャは,コンピュータープログラムを作る時の「設計図」のようなものだ.
-
層分け: プログラムを「たまねぎの皮」のように層に分ける.
- 中心: プログラムの一番大切な部分(例:ゲームのルール)
- 外側: 技術的な部分(例:画面の表示方法,データの保存方法)
-
依存関係: 外側の層が内側の層を使う. でも,内側は外側のことを知らない.
- 例: ゲームのルールは,画面の表示方法を知らなくても良い
-
大切な部分を守る: プログラムの心臓部を,技術の変化から守る.
- 例: 画面の表示方法が変わっても,ゲームのルールは変わらない
-
テストしやすい: 各層が別々になっているので,一つずつ確認しやすい.
- 例: ゲームのルールだけを,画面なしでテストできる
簡単に言えば,クリーンアーキテクチャは大切な部分を真ん中に置いて,それを外側から守るやり方だ.
DDD(ドメイン駆動設計)とは
DDDは,複雑な仕組みをプログラムで表現する方法だ.
-
核心重視: プログラムの中心に,そのプログラムの一番大切な部分を置く.
- 例: 料理レシピアプリなら,「レシピ」が中心
-
共通言語: プログラムを作る人と,そのプログラムを使う専門家が,同じ言葉で話す.
- 例: 料理の専門家とプログラマーが「材料」「手順」という同じ言葉を使う
-
分けて考える: 大きなプログラムを,関連はあるけど別々に考えられる部分に分ける.
- 例: 「レシピ管理」「買い物リスト」「調理タイマー」を別々に考える
-
仕組みを形にする: 現実の世界の仕組みを,そのままプログラムの形にする.
- 例: 現実の「レシピ」をそのままプログラムの中の「レシピクラス」にする
簡単に言えば,DDDは専門家と一緒に,プログラムで表現したい世界の仕組みを理解して,それをそのままプログラムにする方法だ.
クリーンアーキテクチャとDDDを使う利点はいろいろある.
1. 仕組みがはっきりする: DDDでプログラムの核心を理解し,クリーンアーキテクチャでそれを守る.
- 例: レシピの仕組みをよく理解し,それを技術的な部分から守る
2. 変更に強い: 技術が変わっても,プログラムの中心部分は影響を受けにくい.
- 例: データベースを変更しても,レシピの管理方法は変わらない
3. 長持ちする: 要求が変わっても,技術が進歩しても,柔軟に対応できる.
- 例: 新しい料理の種類が増えても,画面デザインが変わっても対応できる
4. チームワークが良くなる: 専門家とプログラマーが同じ言葉で話せて,みんなの役割がはっきりしているので,協力しやすい.
- 例: 料理の専門家とプログラマーが「レシピ」という言葉で共通理解を持てる
つまり,クリーンアーキテクチャやDDDを組み合わせるとプログラムで表現したい世界の仕組みをよく理解して,それを中心に据えた丈夫なプログラムを作ることができる.
バックエンドのディレクトリ構造はさまざまな例があるが,今回はこのようにしてみる.
src/
├── domain/
│ ├── entities/
│ ├── repositories/
│ └── services/
├── application/
│ ├── use-cases/
│ └── interfaces/
├── infrastructure/
│ ├── database/
│ └── repositories/
└── interfaces/
├── controllers/
└── routes/
各層の役割と構成要素
クリーンアーキテクチャとDDDはこのように層に分けて設計する.層がいわゆる玉ねぎの皮にあたるものである.
1. ドメイン層 (domain/)
役割:ビジネスの核心部分を表現し,重要な概念やルールを定義する.
- entities/:ビジネスの主要な対象や概念を表すオブジェクトを定義する.
- repositories/:データの保存や取得に関する抽象的な方法を定義する.
- services/:特定のentityに属さない,ビジネス上の処理や計算を行う機能を実装する.
2. アプリケーション層 (application/)
役割:ユーザーの要求に応じて,ドメイン層の機能を組み合わせて実行する.
- use-cases/:具体的な業務フローを実装する.
- interfaces/:データアクセスや外部サービスとのやり取りに関する抽象的な定義を行う.
3. インフラストラクチャ層 (infrastructure/)
役割:技術的な詳細を実装し,外部システムとの連携を担当する.
- database/:データベースへの接続や設定を管理する.
- repositories/:データの具体的な保存方法や取得方法を実装する.
4. インターフェース層 (interfaces/)
役割:ユーザーや外部システムとのやり取りを担当する.
- controllers/:ユーザーからのリクエストを受け取り,適切な処理を呼び出す.
- routes/:リクエストのURLとcontrollerの紐付けを定義する.
ビジネスロジックとは
プログラムの中心となる重要な処理や判断のルールのこと
例えば
- オンラインショップの場合:
- 商品の在庫管理
- 価格計算(割引適用など)
- 注文処理
- 銀行システムの場合:
- 口座残高の計算
- 利息の計算
- 送金処理
- 予約システムの場合:
- 空き状況の確認
- 重複予約のチェック
- キャンセル料の計算
クリーンアーキテクチャとDDDを組み合わせたAPIの動作の流れ
- ユーザがroutersで設定したURLのエンドポイントにアクセスし,リクエストを送る.
- ユーザーからのリクエストがインターフェース層のcontrollerに届く.
- Controllerが適切なuse-caseを呼び出す.
- Use-caseがドメイン層のentityやserviceを使用してビジネスロジックを実行する.
- 必要に応じて,インフラストラクチャ層のrepositoryを通じてデータの保存や取得を行う.
- 処理結果がインターフェース層に返され,ユーザーに適切な形で提示される.
Dockerとは
本記事で紹介するAPIは環境構築やデータベース管理システム(DBMS)との通信を行うためにDockerを用いるためDockerの説明をする.
以下の説明はこの記事で説明していることを簡単に要約したもののため,詳しい説明やコード例はこちらを参考にしてほしい.
アプリケーションをコンテナという隔離された環境にパッケージ化するためのオープンソースのプラットフォーム.
開発,デプロイ,実行が簡単にできるようになる.
Dockerを使うメリット
-
環境一貫性とポータビリティ
Docker コンテナはアプリケーションとその依存関係を一つのパッケージにカプセル化する.これにより,開発,テスト,本番環境間での「動作するはずが動かない」という問題を解決する.また,Docker コンテナはどのプラットフォーム(Linux, Windows, macOSなど)でも実行できる. -
開発の効率化
Dockerを使用することで,必要な環境をすぐに用意できる.また、再利用,配布をすれば,開発プロセスの効率化につながる.Dockerはマイクロサービスアーキテクチャの採用を助ける.各マイクロサービスを個別のコンテナとして分離・運用することが容易になる. -
インフラストラクチャの最適化
Docker コンテナは軽量で,起動が速く,少ないリソースで多くのコンテナを同時に実行できる.これにより,ハードウェアリソースをより効率的に使用できる.Docker コンテナは簡単にスケールアップおよびスケールダウンが可能.クラウド環境やオーケストレーションツール(Kubernetesなど)と組み合わせることで、自動スケーリングやロードバランシングが容易になる.
-
Docker Container(コンテナ)
Docker Containerとは,アプリケーションとその実行に必要なすべての依存関係を含む軽量な,スタンドアローンの実行可能パッケージ.Docker コンテナは,Docker エンジンがインストールされた任意のシステム上で動作するように設計されており,これをコンテナ仮想化技術という
ある入れ物(コンテナ)の中に
OS
依存関係
ミドルウェア
アプリケーション
が入っており,これでアプリケーション単位での依存関係(パッケージ,フレームワーク,ライブラリ)の管理が可能になる.
これを他の人でも共有して開発できれば簡単に共同開発できる
Docker イメージは,Docker コンテナを実行するための静的なテンプレート.これは,コンテナを実行する際に必要なすべてのファイル,コード,ライブラリ,環境設定,および依存関係が含まれている.これを実行すると,コンテナが形成される.
Dockerイメージを生成するためにDockerfile
を作成する.これはDocker イメージの設計図となる.
Node.jsでのDockerfileは後で紹介する
-
Docker Volume(ボリューム)
データを保存し永続化するための場所.コンテナは通常,削除されるとその内部で作成されたファイルも失われる.しかし,ボリュームを使用すると,これらのデータをコンテナのライフサイクルとは独立して保存できる. -
複数のコンテナを組み合わせるには
複数のコンテナを組み合わせて使用することは非常に一般的であり,Dockerの主要な利用シナリオの一つである.特に,マイクロサービスアーキテクチャの実装や,異なるサービスが連携して動作するアプリケーションを開発する場合に有効である.例えばアプリケーションが二つ以上あるときや,データベースと組み合わせたい時に使われる.
複数のコンテナやボリュームなどを組み合わせるならばdocker-compose.yml
を作成する.
NodeアプリとMysqlを組み合わせるための
docker-compose.yml
は後ほど紹介する
PythonまたはGoアプリケーションとMySQLを組み合わせる例は以下の記事でまとめたので参考にしてほしい.
実装
今回はユーザ情報とそのユーザー作成するブログの情報をやり取りするAPIを作成する.
必要なプラットフォーム,ライブラリ,などをPC上にインストールする
macの方は以下コマンドでインストールできる.
他の方は公式サイトで以下の3つをインストールしてほしい.(場合によってはパスを通さないといけないかも)
ここからの開発はVSCodeなどのIDE(統合開発環境)上で行うようにしてほしい.
1.Node.js
brew install nodebrew
2.npm
npm install -g npm
3.Docker
brew install --cask docker
ここから実際にプロジェクトを作成し開発していく.
ルートディレクトリを作成
mkdir my-node-project
cd my-node-project
package.jsonファイルがを作成し,node.jsプロジェクトを作成する
npm init -y
package.jsonとは
Node.jsプロジェクトの設定ファイル.プロジェクト名、バージョン、依存関係、スクリプトなどの重要な情報を含む.npm initで自動生成され,手動で編集も可能.プロジェクトの管理や他の開発者との共有に不可欠.
アプリのエントリポイントであるapp.jsを作成する
touch app.ts
エントリーポイントとは
Node.jsアプリケーションの実行開始点となるファイル.通常、app.tsやindex.tsと名付けられ,アプリケーションの初期化や設定をう行う.package.jsonの"main"フィールドで指定され,node コマンドで直接実行されるファイル.
package.jsonのscriptセクションに以下の内容を追加する.
"scripts": {
"start": "node app.ts"
}
Dockerコンテナ
実装する際のディレクトリ構造は以下の通りとする.
.
├── src/
│ ├── domain/
│ │ ├── entities/
│ │ │ ├── User.ts
│ │ │ └── BlogPost.ts
│ │ ├── repositories/
│ │ │ ├── UserRepository.ts
│ │ │ └── BlogPostRepository.ts
│ │ └── services/
│ │ ├── UserService.ts
│ │ └── BlogPostService.ts
│ ├── application/
│ │ ├── use-cases/
│ │ │ ├── CreateUser.ts
│ │ │ ├── GetUser.ts
│ │ │ ├── CreateBlogPost.ts
│ │ │ └── GetBlogPost.ts
│ │ └── interfaces/
│ │ ├── IUserRepository.ts
│ │ └── IBlogPostRepository.ts
│ ├── infrastructure/
│ │ ├── database/
│ │ │ └── mysql.ts
│ │ └── repositories/
│ │ ├── MysqlUserRepository.ts
│ │ └── MysqlBlogPostRepository.ts
│ └── interfaces/
│ ├── controllers/
│ │ ├── UserController.ts
│ │ └── BlogPostController.ts
│ └── routes/
│ ├── userRoutes.ts
│ └── blogPostRoutes.ts
├── Dockerfile
├── docker-compose.yml
├── package.json
├── tsconfig.json
└── README.md
まずアプリケーションコンテナのイメージを作成するためのDockerfileを作成する.
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 8080
CMD ["npm", "start"]
mysqlコンテナとアプリケーションコンテナを結びつける用のdocker-compose.ymlファイル
version: '3'
services:
app:
build: .
ports:
- "8080:8080"
depends_on:
- db
volumes:
- .:/app
- /app/node_modules
db:
image: mysql:8
volumes:
- ./mysql-data:/var/lib/mysql
ports:
- "3306:3306"
volumes:
mysql-data:
データベース接続情報などの環境変数のファイルを作成する.これはgitで管理しないことをお勧めする.(.gitignoreファイルに.envを書いておこう)
# API
DB_HOST=localhost
DB_USER=user
DB_PASSWORD=password
DB_NAME=db
# その他の環境変数
PORT=3000
NODE_ENV=development
# MySQL
MYSQL_ROOT_PASSWORD=password
MYSQL_DATABASE=db
domain層entity
エンティティは,ビジネスロジックの中核を表現するオブジェクトである.ユーザーや商品など,システムの主要な概念を表し,その属性とビジネスルールを定義する.これらは外部の依存関係を持たず,純粋なドメインロジックを含む.
export class User {
constructor(
public id: string,
public username: string,
public email: string,
public password: string,
public createdAt: Date,
public updatedAt: Date
) {}
/**
* ユーザー情報を更新する
* @param username 新しいユーザー名
* @param email 新しいメールアドレス
* @returns void
*/
updateInfo(username: string, email: string): void {
this.username = username;
this.email = email;
this.updatedAt = new Date();
}
/**
* パスワードを更新する
* @param newPassword 新しいパスワード
* @returns void
*/
updatePassword(newPassword: string): void {
this.password = newPassword;
this.updatedAt = new Date();
}
}
export class BlogPost {
constructor(
public id: string,
public title: string,
public content: string,
public authorId: string,
public createdAt: Date,
public updatedAt: Date,
public publishedAt: Date | null
) {}
/**
* ブログ投稿を更新する
* @param title 新しいタイトル
* @param content 新しい内容
* @returns void
*/
update(title: string, content: string): void {
this.title = title;
this.content = content;
this.updatedAt = new Date();
}
/**
* ブログ投稿を公開する
* @returns void
*/
publish(): void {
this.publishedAt = new Date();
this.updatedAt = new Date();
}
/**
* ブログ投稿の公開を取り消す
* @returns void
*/
unpublish(): void {
this.publishedAt = null;
this.updatedAt = new Date();
}
}
domain層repository
リポジトリは,エンティティの永続化と取得のための抽象インターフェースを定義する.データの保存や検索のメソッドを宣言するが,具体的な実装は含まない.これにより,ドメイン層はデータアクセスの詳細から独立し,柔軟性と交換可能性が確保される.
import { User } from '../entities/User';
export interface UserRepository {
/**
* ユーザーを作成する
* @param user 作成するユーザーオブジェクト
* @returns 作成されたユーザー
*/
create(user: User): Promise<User>;
/**
* IDでユーザーを取得する
* @param id ユーザーID
* @returns 見つかったユーザー、見つからない場合はnull
*/
findById(id: string): Promise<User | null>;
/**
* メールアドレスでユーザーを取得する
* @param email メールアドレス
* @returns 見つかったユーザー、見つからない場合はnull
*/
findByEmail(email: string): Promise<User | null>;
/**
* ユーザーを更新する
* @param user 更新するユーザーオブジェクト
* @returns 更新されたユーザー
*/
update(user: User): Promise<User>;
/**
* ユーザーを削除する
* @param id 削除するユーザーのID
* @returns void
*/
delete(id: string): Promise<void>;
}
import { BlogPost } from '../entities/BlogPost';
export interface BlogPostRepository {
/**
* ブログ投稿を作成する
* @param blogPost 作成するブログ投稿オブジェクト
* @returns 作成されたブログ投稿
*/
create(blogPost: BlogPost): Promise<BlogPost>;
/**
* IDでブログ投稿を取得する
* @param id ブログ投稿ID
* @returns 見つかったブログ投稿、見つからない場合はnull
*/
findById(id: string): Promise<BlogPost | null>;
/**
* 著者IDで全てのブログ投稿を取得する
* @param authorId 著者ID
* @returns 著者のブログ投稿の配列
*/
findByAuthorId(authorId: string): Promise<BlogPost[]>;
/**
* ブログ投稿を更新する
* @param blogPost 更新するブログ投稿オブジェクト
* @returns 更新されたブログ投稿
*/
update(blogPost: BlogPost): Promise<BlogPost>;
/**
* ブログ投稿を削除する
* @param id 削除するブログ投稿のID
* @returns void
*/
delete(id: string): Promise<void>;
}
domain層service
ドメインサービスは,特定のエンティティに属さないビジネスロジックを実装する.複数のエンティティ間の相互作用や,複雑な計算,外部システムとの連携などを担当する.エンティティの振る舞いを補完し,ドメインの一貫性を維持する.
import { User } from '../entities/User';
import { UserRepository } from '../repositories/UserRepository';
export class UserService {
constructor(private userRepository: UserRepository) {}
/**
* ユーザーを作成する
* @param userData ユーザー作成に必要なデータ
* @returns 作成されたユーザー
*/
async createUser(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
const newUser = new User(
Date.now().toString(), // 簡易的なID生成
userData.username,
userData.email,
userData.password,
new Date(),
new Date()
);
return this.userRepository.create(newUser);
}
/**
* IDでユーザーを取得する
* @param id ユーザーID
* @returns 見つかったユーザー、見つからない場合はnull
*/
async getUserById(id: string): Promise<User | null> {
return this.userRepository.findById(id);
}
/**
* ユーザー情報を更新する
* @param id ユーザーID
* @param userData 更新するユーザーデータ
* @returns 更新されたユーザー
*/
async updateUser(id: string, userData: Partial<User>): Promise<User> {
const user = await this.userRepository.findById(id);
if (!user) {
throw new Error('User not found');
}
Object.assign(user, userData);
user.updatedAt = new Date();
return this.userRepository.update(user);
}
/**
* ユーザーを削除する
* @param id 削除するユーザーのID
* @returns void
*/
async deleteUser(id: string): Promise<void> {
await this.userRepository.delete(id);
}
}
import { BlogPost } from '../entities/BlogPost';
import { BlogPostRepository } from '../repositories/BlogPostRepository';
export class BlogPostService {
constructor(private blogPostRepository: BlogPostRepository) {}
/**
* ブログ投稿を作成する
* @param blogPostData ブログ投稿作成に必要なデータ
* @returns 作成されたブログ投稿
*/
async createBlogPost(blogPostData: Omit<BlogPost, 'id' | 'createdAt' | 'updatedAt' | 'publishedAt'>): Promise<BlogPost> {
const newBlogPost = new BlogPost(
Date.now().toString(), // 簡易的なID生成
blogPostData.title,
blogPostData.content,
blogPostData.authorId,
new Date(),
new Date(),
null
);
return this.blogPostRepository.create(newBlogPost);
}
/**
* IDでブログ投稿を取得する
* @param id ブログ投稿ID
* @returns 見つかったブログ投稿、見つからない場合はnull
*/
async getBlogPostById(id: string): Promise<BlogPost | null> {
return this.blogPostRepository.findById(id);
}
/**
* 著者IDで全てのブログ投稿を取得する
* @param authorId 著者ID
* @returns 著者のブログ投稿の配列
*/
async getBlogPostsByAuthorId(authorId: string): Promise<BlogPost[]> {
return this.blogPostRepository.findByAuthorId(authorId);
}
/**
* ブログ投稿を更新する
* @param id ブログ投稿ID
* @param blogPostData 更新するブログ投稿データ
* @returns 更新されたブログ投稿
*/
async updateBlogPost(id: string, blogPostData: Partial<BlogPost>): Promise<BlogPost> {
const blogPost = await this.blogPostRepository.findById(id);
if (!blogPost) {
throw new Error('Blog post not found');
}
Object.assign(blogPost, blogPostData);
blogPost.updatedAt = new Date();
return this.blogPostRepository.update(blogPost);
}
/**
* ブログ投稿を削除する
* @param id 削除するブログ投稿のID
* @returns void
*/
async deleteBlogPost(id: string): Promise<void> {
await this.blogPostRepository.delete(id);
}
/**
* ブログ投稿を公開する
* @param id 公開するブログ投稿のID
* @returns 公開されたブログ投稿
*/
async publishBlogPost(id: string): Promise<BlogPost> {
const blogPost = await this.blogPostRepository.findById(id);
if (!blogPost) {
throw new Error('Blog post not found');
}
blogPost.publish();
return this.blogPostRepository.update(blogPost);
}
/**
* ブログ投稿の公開を取り消す
* @param id 公開を取り消すブログ投稿のID
* @returns 公開が取り消されたブログ投稿
*/
async unpublishBlogPost(id: string): Promise<BlogPost> {
const blogPost = await this.blogPostRepository.findById(id);
if (!blogPost) {
throw new Error('Blog post not found');
}
blogPost.unpublish();
return this.blogPostRepository.update(blogPost);
}
}
application層usecase
ユースケースは,アプリケーションの具体的な機能や操作を表現する.ドメイン層のエンティティやサービスを組み合わせて,特定のビジネスプロセスを実行する.入力の検証,トランザクション管理,結果の整形などを行い,アプリケーションの動作を制御する.
import { User } from '../../domain/entities/User';
import { UserService } from '../../domain/services/UserService';
export class CreateUser {
constructor(private userService: UserService) {}
/**
* 新しいユーザーを作成する
* @param userData ユーザー作成に必要なデータ
* @returns 作成されたユーザー
*/
async execute(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
return this.userService.createUser(userData);
}
}
import { User } from '../../domain/entities/User';
import { UserService } from '../../domain/services/UserService';
export class GetUser {
constructor(private userService: UserService) {}
/**
* IDでユーザーを取得する
* @param id ユーザーID
* @returns 見つかったユーザー、見つからない場合はnull
*/
async execute(id: string): Promise<User | null> {
return this.userService.getUserById(id);
}
}
import { BlogPost } from '../../domain/entities/BlogPost';
import { BlogPostService } from '../../domain/services/BlogPostService';
export class CreateBlogPost {
constructor(private blogPostService: BlogPostService) {}
/**
* 新しいブログ投稿を作成する
* @param blogPostData ブログ投稿作成に必要なデータ
* @returns 作成されたブログ投稿
*/
async execute(blogPostData: Omit<BlogPost, 'id' | 'createdAt' | 'updatedAt' | 'publishedAt'>): Promise<BlogPost> {
return this.blogPostService.createBlogPost(blogPostData);
}
}
import { BlogPost } from '../../domain/entities/BlogPost';
import { BlogPostService } from '../../domain/services/BlogPostService';
export class GetBlogPost {
constructor(private blogPostService: BlogPostService) {}
/**
* IDでブログ投稿を取得する
* @param id ブログ投稿ID
* @returns 見つかったブログ投稿、見つからない場合はnull
*/
async execute(id: string): Promise<BlogPost | null> {
return this.blogPostService.getBlogPostById(id);
}
}
application層interface
アプリケーション層のインターフェースは,ユースケースとの対話方法を定義する.外部からのリクエストをユースケースに適した形に変換し,結果を適切な形式で返す.これにより,アプリケーション層と外部システムの疎結合が実現される.
import { User } from '../../domain/entities/User';
export interface IUserRepository {
create(user: User): Promise<User>;
findById(id: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
update(user: User): Promise<User>;
delete(id: string): Promise<void>;
}
import { BlogPost } from '../../domain/entities/BlogPost';
export interface IBlogPostRepository {
create(blogPost: BlogPost): Promise<BlogPost>;
findById(id: string): Promise<BlogPost | null>;
findByAuthorId(authorId: string): Promise<BlogPost[]>;
update(blogPost: BlogPost): Promise<BlogPost>;
delete(id: string): Promise<void>;
}
infrastructure層database
データベース層は,実際のデータベース操作を担当する.データベース接続の確立,クエリの実行,トランザクション管理などを行う.ORMやクエリビルダーの使用,SQL文の直接実行など,具体的なデータアクセス技術を実装する.ここでは例として初回実行時のテーブル作成も実装している.
SQLについては以下の記事で詳しく解説している.SQLはイメージとしてDB操作用の言語だと思ってほしい.
import mysql from 'mysql2/promise';
export const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
/**
* データベース接続をテストし、必要に応じてテーブルを作成する
* @returns void
*/
export async function initializeDatabase(): Promise<void> {
try {
const connection = await pool.getConnection();
console.log('Successfully connected to the database.');
// テーブルの存在確認と作成
await createTablesIfNotExist(connection);
connection.release();
} catch (error) {
console.error('Failed to initialize the database:', error);
throw error;
}
}
async function createTablesIfNotExist(connection: mysql.PoolConnection): Promise<void> {
const createUsersTable = `
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(36) PRIMARY KEY,
username VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
`;
const createBlogPostsTable = `
CREATE TABLE IF NOT EXISTS blog_posts (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
author_id VARCHAR(36),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
published_at TIMESTAMP NULL,
FOREIGN KEY (author_id) REFERENCES users(id)
)
`;
await connection.query(createUsersTable);
await connection.query(createBlogPostsTable);
console.log('Tables created or already exist.');
}
infrastructure層repository
リポジトリの具体的な実装を提供する.ドメイン層で定義されたリポジトリインターフェースを実装し,実際のデータベース操作を行う.データの永続化,検索,更新などの処理を,選択したデータベース技術に合わせて実装する.
import { User } from '../../domain/entities/User';
import { UserRepository } from '../../domain/repositories/UserRepository';
import { pool } from '../database/mysql';
export class MysqlUserRepository implements UserRepository {
/**
* ユーザーを作成する
* @param user 作成するユーザーオブジェクト
* @returns 作成されたユーザー
*/
async create(user: User): Promise<User> {
const [result] = await pool.execute(
'INSERT INTO users (id, username, email, password, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[user.id, user.username, user.email, user.password, user.createdAt, user.updatedAt]
);
return user;
}
/**
* IDでユーザーを取得する
* @param id ユーザーID
* @returns 見つかったユーザー、見つからない場合はnull
*/
async findById(id: string): Promise<User | null> {
const [rows] = await pool.execute('SELECT * FROM users WHERE id = ?', [id]);
if (Array.isArray(rows) && rows.length > 0) {
const userData = rows[0] as any;
return new User(
userData.id,
userData.username,
userData.email,
userData.password,
new Date(userData.created_at),
new Date(userData.updated_at)
);
}
return null;
}
/**
* メールアドレスでユーザーを取得する
* @param email メールアドレス
* @returns 見つかったユーザー、見つからない場合はnull
*/
async findByEmail(email: string): Promise<User | null> {
const [rows] = await pool.execute('SELECT * FROM users WHERE email = ?', [email]);
if (Array.isArray(rows) && rows.length > 0) {
const userData = rows[0] as any;
return new User(
userData.id,
userData.username,
userData.email,
userData.password,
new Date(userData.created_at),
new Date(userData.updated_at)
);
}
return null;
}
/**
* ユーザーを更新する
* @param user 更新するユーザーオブジェクト
* @returns 更新されたユーザー
*/
async update(user: User): Promise<User> {
await pool.execute(
'UPDATE users SET username = ?, email = ?, password = ?, updated_at = ? WHERE id = ?',
[user.username, user.email, user.password, user.updatedAt, user.id]
);
return user;
}
/**
* ユーザーを削除する
* @param id 削除するユーザーのID
* @returns void
*/
async delete(id: string): Promise<void> {
await pool.execute('DELETE FROM users WHERE id = ?', [id]);
}
}
import { BlogPost } from '../../domain/entities/BlogPost';
import { BlogPostRepository } from '../../domain/repositories/BlogPostRepository';
import { pool } from '../database/mysql';
export class MysqlBlogPostRepository implements BlogPostRepository {
/**
* ブログ投稿を作成する
* @param blogPost 作成するブログ投稿オブジェクト
* @returns 作成されたブログ投稿
*/
async create(blogPost: BlogPost): Promise<BlogPost> {
const [result] = await pool.execute(
'INSERT INTO blog_posts (id, title, content, author_id, created_at, updated_at, published_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
[blogPost.id, blogPost.title, blogPost.content, blogPost.authorId, blogPost.createdAt, blogPost.updatedAt, blogPost.publishedAt]
);
return blogPost;
}
/**
* IDでブログ投稿を取得する
* @param id ブログ投稿ID
* @returns 見つかったブログ投稿、見つからない場合はnull
*/
async findById(id: string): Promise<BlogPost | null> {
const [rows] = await pool.execute('SELECT * FROM blog_posts WHERE id = ?', [id]);
if (Array.isArray(rows) && rows.length > 0) {
const blogPostData = rows[0] as any;
return new BlogPost(
blogPostData.id,
blogPostData.title,
blogPostData.content,
blogPostData.author_id,
new Date(blogPostData.created_at),
new Date(blogPostData.updated_at),
blogPostData.published_at ? new Date(blogPostData.published_at) : null
);
}
return null;
}
/**
* 著者IDで全てのブログ投稿を取得する
* @param authorId 著者ID
* @returns 著者のブログ投稿の配列
*/
async findByAuthorId(authorId: string): Promise<BlogPost[]> {
const [rows] = await pool.execute('SELECT * FROM blog_posts WHERE author_id = ?', [authorId]);
if (Array.isArray(rows)) {
return rows.map((blogPostData: any) => new BlogPost(
blogPostData.id,
blogPostData.title,
blogPostData.content,
blogPostData.author_id,
new Date(blogPostData.created_at),
new Date(blogPostData.updated_at),
blogPostData.published_at ? new Date(blogPostData.published_at) : null
));
}
return [];
}
/**
* ブログ投稿を更新する
* @param blogPost 更新するブログ投稿オブジェクト
* @returns 更新されたブログ投稿
*/
async update(blogPost: BlogPost): Promise<BlogPost> {
await pool.execute(
'UPDATE blog_posts SET title = ?, content = ?, updated_at = ?, published_at = ? WHERE id = ?',
[blogPost.title, blogPost.content, blogPost.updatedAt, blogPost.publishedAt, blogPost.id]
);
return blogPost;
}
/**
* ブログ投稿を削除する
* @param id 削除するブログ投稿のID
* @returns void
*/
async delete(id: string): Promise<void> {
await pool.execute('DELETE FROM blog_posts WHERE id = ?', [id]);
}
}
interfaces層controllers
コントローラーは,外部からのリクエストを受け取り,適切なユースケースを呼び出す.HTTPリクエストのパラメータを解析し,ユースケースに渡す入力を準備する.ユースケースの実行結果を受け取り,適切なレスポンスを生成して返す.
import { Request, Response } from 'express';
import { CreateUser } from '../../application/use-cases/CreateUser';
import { GetUser } from '../../application/use-cases/GetUser';
export class UserController {
constructor(
private createUserUseCase: CreateUser,
private getUserUseCase: GetUser
) {}
/**
* ユーザーを作成する
* @param req リクエストオブジェクト
* @param res レスポンスオブジェクト
*/
async createUser(req: Request, res: Response): Promise<void> {
try {
const { username, email, password } = req.body;
const user = await this.createUserUseCase.execute({ username, email, password });
res.status(201).json(user);
} catch (error) {
console.error('Error creating user:', error);
res.status(500).json({ message: 'Internal server error' });
}
}
/**
* ユーザーを取得する
* @param req リクエストオブジェクト
* @param res レスポンスオブジェクト
*/
async getUser(req: Request, res: Response): Promise<void> {
try {
const userId = req.params.id;
const user = await this.getUserUseCase.execute(userId);
if (user) {
res.json(user);
} else {
res.status(404).json({ message: 'User not found' });
}
} catch (error) {
console.error('Error getting user:', error);
res.status(500).json({ message: 'Internal server error' });
}
}
}
import { Request, Response } from 'express';
import { CreateBlogPost } from '../../application/use-cases/CreateBlogPost';
import { GetBlogPost } from '../../application/use-cases/GetBlogPost';
export class BlogPostController {
constructor(
private createBlogPostUseCase: CreateBlogPost,
private getBlogPostUseCase: GetBlogPost
) {}
/**
* ブログ投稿を作成する
* @param req リクエストオブジェクト
* @param res レスポンスオブジェクト
*/
async createBlogPost(req: Request, res: Response): Promise<void> {
try {
const { title, content, authorId } = req.body;
const blogPost = await this.createBlogPostUseCase.execute({ title, content, authorId });
res.status(201).json(blogPost);
} catch (error) {
console.error('Error creating blog post:', error);
res.status(500).json({ message: 'Internal server error' });
}
}
/**
* ブログ投稿を取得する
* @param req リクエストオブジェクト
* @param res レスポンスオブジェクト
*/
async getBlogPost(req: Request, res: Response): Promise<void> {
try {
const blogPostId = req.params.id;
const blogPost = await this.getBlogPostUseCase.execute(blogPostId);
if (blogPost) {
res.json(blogPost);
} else {
res.status(404).json({ message: 'Blog post not found' });
}
} catch (error) {
console.error('Error getting blog post:', error);
res.status(500).json({ message: 'Internal server error' });
}
}
}
interfaces層routes
ルーティングは,外部からのリクエストを適切なコントローラーにマッピングする.URLパターンとHTTPメソッドに基づいて,どのコントローラーのどのメソッドを呼び出すかを定義する.エンドポイントの構造を整理し,アプリケーションの外部インターフェースを形成する.
import { Router } from 'express';
import { UserController } from '../controllers/UserController';
export function userRoutes(userController: UserController): Router {
const router = Router();
router.post('/', userController.createUser.bind(userController));
router.get('/:id', userController.getUser.bind(userController));
return router;
}
import { Router } from 'express';
import { BlogPostController } from '../controllers/BlogPostController';
export function blogPostRoutes(blogPostController: BlogPostController): Router {
const router = Router();
router.post('/', blogPostController.createBlogPost.bind(blogPostController));
router.get('/:id', blogPostController.getBlogPost.bind(blogPostController));
return router;
}
エントリポイント
import express from 'express';
import { userRoutes } from './interfaces/routes/userRoutes';
import { blogPostRoutes } from './interfaces/routes/blogPostRoutes';
import { UserController } from './interfaces/controllers/UserController';
import { BlogPostController } from './interfaces/controllers/BlogPostController';
import { CreateUser } from './application/use-cases/CreateUser';
import { GetUser } from './application/use-cases/GetUser';
import { CreateBlogPost } from './application/use-cases/CreateBlogPost';
import { GetBlogPost } from './application/use-cases/GetBlogPost';
import { UserService } from './domain/services/UserService';
import { BlogPostService } from './domain/services/BlogPostService';
import { MysqlUserRepository } from './infrastructure/repositories/MysqlUserRepository';
import { MysqlBlogPostRepository } from './infrastructure/repositories/MysqlBlogPostRepository';
import { initializeDatabase } from './infrastructure/database/mysql';
const app = express();
app.use(express.json());
async function startServer() {
try {
// データベースの初期化
await initializeDatabase();
// リポジトリの初期化
const userRepository = new MysqlUserRepository();
const blogPostRepository = new MysqlBlogPostRepository();
// サービスの初期化
const userService = new UserService(userRepository);
const blogPostService = new BlogPostService(blogPostRepository);
// ユースケースの初期化
const createUser = new CreateUser(userService);
const getUser = new GetUser(userService);
const createBlogPost = new CreateBlogPost(blogPostService);
const getBlogPost = new GetBlogPost(blogPostService);
// コントローラーの初期化
const userController = new UserController(createUser, getUser);
const blogPostController = new BlogPostController(createBlogPost, getBlogPost);
// ルーティングの設定
app.use('/users', userRoutes(userController));
app.use('/blog-posts', blogPostRoutes(blogPostController));
// サーバーの起動
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
} catch (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
}
startServer();
export default app;
実行コマンドは以下のコマンドでdockerコンテナを立ち上げる形でアプリが動くようになる.
docker compose up
これでクライアント(フロントエンド)側で
http://localhost:8080/<任意のエンドポイント>
にリクエストを送れば良い.
取得したいのならばGETリクエスト
を
送信ならPOST(編集時はPUT,PATCH)リクエスト
を
削除ならDELETEリクエスト
を送れば良い.
フロントとバックエンドの連携方法
ここからは作成したAPIをフロントエンド側でアクセスするための方法を記載する.
フロントからAPIにアクセスする際はdockerコンテナは立ち上げたままにしよう.
ここでは開発環境としてローカルのコンテナにアクセスすることを想定とする.デプロイとかは一切考えていないのでデプロイしたい方はこちらの記事を参考にしてほしい
ここからのコードはバックエンド側のコードではなくフロントエンド(React)側のコードなので注意
フロントのsrcディレクトリにapiディレクトリを作成し,バックエンドで定義したAPIにアクセスできるようにしても良い.
import { apiClient } from "./apiClient";
export type Data = {
id: number;
name: string;
};
export type DataCreateRequest = {
name: string;
};
export const create = async (params: DataCreateRequest) => {
const response = await apiClient.post(`/api/data`, params);
const data = response.data as data;
return data;
};
これでhttp:\//localhost:8080/api/data
にparams(JSONデータ)を送るPOSTリクエストを送信できる.
このparamsはページのコンポーネントで指定すれば良い
またapiに繋ぐためのファイルapiClient.tsも必要になる.
import axios from "axios";
export const envConfig = {
REACT_APP_PUBLIC_BACKEND_URL: process.env.REACT_APP_PUBLIC_BACKEND_URL,
};
export const apiClient = axios.create({
baseURL: envConfig.REACT_APP_PUBLIC_BACKEND_URL,
headers: {
"Content-Type": "application/json",
},
withCredentials: true,
});
export const apiClientMultipart = axios.create({
baseURL: envConfig.REACT_APP_PUBLIC_BACKEND_URL,
headers: {
"Content-Type": "multipart/form-data",
},
withCredentials: true,
});
環境変数(バックエンドで指定したapiのホスト名とポート)
REACT_APP_PUBLIC_BACKEND_URL=http://localhost:8080
例としてページのコンポーネントではこのroutes.tsで作成したエンドポイントを使ってリクエストを送る.
この例では簡単なフォームのコンポーネントを作成している.
import React, { useState } from 'react';
import { create, DataCreateRequest } from '../api/routes';
const CreateDataComponent: React.FC = () => {
const [formData, setFormData] = useState<DataCreateRequest>({
name: '',
});
const [isLoading, setIsLoading] = useState(false);
const [message, setMessage] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prevState => ({
...prevState,
[name]: value
}));
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setMessage('');
try {
const result = await create(formData);
setMessage(`Data created successfully with ID: ${result.id}`);
setFormData({ name: '' }); // Reset form after successful creation
} catch (error) {
setMessage('Error creating data. Please try again.');
console.error('Error:', error);
} finally {
setIsLoading(false);
}
};
return (
<div>
<h2>Create New Data</h2>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name:</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
</div>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Creating...' : 'Create'}
</button>
</form>
{message && <p>{message}</p>}
</div>
);
};
export default CreateDataComponent;
またターミナル等のコマンドで
curl -X GET 'http://localhost:8080/<任意のエンドポイント>'
curl -X POST 'http://localhost:8080/<任意のエンドポイント>' -d '<JSONデータ>'
とリクエストを送っても良い.JSONデータが返ってくる.
また,今回はSQLを直接書いたが,SQLを直接書かずにORM(Object-Relational Mapping)を使ってSQLを使わずに書くこともできる.
このようにTypeScriptではさまざまなことができ,フロントからバックまで開発することができる.