2025-3I プログラミング3 第03回 講義資料

2025年10月16日(木)5・6時限

1 今回の授業概要と連絡

本科目は学修単位科目です。1回の授業あたり、皆さんが「4時間相当の授業時間外学習」をすることを前提 としたボリューム、展開速度となっています。いろいろと忙しいとは思いますが、授業以外の時間も確保して取り組んでください。

1.1 小テスト❷

「小テスト❷」を実施します。筆記用具を準備しておいてください。

1.2 ここまでの流れ

後期中間試験ぐらいまでの授業の流れ (予定)

  1. モダンTypeScript基礎学習のための環境構築
  2. TypeScript基礎学習 ← 前回と今回の授業のメイン
    • React / Next.js 開発に関連する文法や機能だけを集中的に学びます
  3. Reactを使ったTodoアプリのための環境構築 ← 次回
  4. Todoアプリ開発(Reactによるフロントエンド開発)のチュートリアル
  5. Todoアプリのカスタマイズや作り込み → 後期前半の大課題

まずは、次のような「📝Todoアプリの開発 (Reactを採用したフロントエンド開発)」目標とします。

2 VSCodeの関連のTips

次回に向けて、React開発に有用な拡張機能などをインストールしておいてください。

2.1 VSCodeの再読み込み

[Ctrl]+[Shift]+[P]コマンドパレット を起動するか、VSCode上部の検索欄をクリックして >Developer: Reload Window と入力すると「プロジェクトファイルの再読み込み」が行われます。

2回目以降は、履歴から >Developer: Reload Window を選択して実行可能です。

img

特に、今後のReact開発では、import 文で 適切にファイルパスを設定しているにも関わらず「赤波線」でエラーが表示されること が多々あります (やっかいなことにVSCodeを再起動してもキャッシュが残っており解決しません)。そのようなときは、この Reload Window で解決します。

2.2 ファイルパス補完の拡張機能

VSCodeの拡張機能として Path Intellisense (識別子:christian-kohler.path-intellisense) をインストールしておくと、プログラムのなかで ファイルパス を入力するときに補完が効いて便利です。

ファイルパス補完の拡張機能は「Path Intellisense」の他にも様々なものがあるので、自分で使いやすいものを探してみてください。

img

2.3 React開発の支援機能

React関連のコードスニペットを挿入する拡張機能として ES7+React/Redux/Reac1t-Native snippets (識別子: dsznajder.es7-react-js-snippets) を VScode にインストールしておいてください (次回以降の講義では、この拡張機能がインストールされている前提で解説している部分があります)。

この拡張機能を導入すると rafce と入力するだけで、Reactコンポーネントのスニペット (=プログラムコードの定型的な断片、雛形) をエディタに挿入してくれます。

img

この拡張機能の概要はこちらを参照してください。

2.4 コードチェックツールの拡張機能

ESLint (識別子: dbaeumer.vscode-eslint) は、ECMAScript用 (=TypeScript/JavaScript用) の コードチェックツール に関する拡張機能です。こちらも、次回以降の講義で利用していく予定なのであらかじめインストールしておいてください。

この拡張機能の概要はこちらを参照してください。

2.5 Tailwind用のインテリセンス

Tailwind CSS IntelliSense (識別子 bradlc.vscode-tailwindcss) はTailwind CSSという CSSフレームワークに関連したインテリセンス (コードの自動補完ツール) です。本授業でのウェブアプリ開発でも Tailwind CSS を使用していくので、その開発体験を向上させるためにインストールしておいてください。

この拡張機能の概要はこちらを参照してください。

(プロンプト例)

CSSフレームワーク とはなんですか。普通に CSS ファイルを書くのと何が違いますか。

Tailwind CSS とは何ですか。

3 前回の復習

第02回講義のReactにおける状態 (オブジェクト) の変更の検知 ~概要~について、ある程度、理解していることを前提とします。

3.1 オブジェクトのプロパティを変更するための2つのアプローチ

前回の講義では、ユーザー定義型の オブジェクトを更新 する場合 (主としてオブジェクトのプロパティ(属性)を変更する場合) には、似ているようで大きく異なる2つの方法 (アプローチ) が存在することを解説しました。

3.1.1 ミュータブルなオブジェクト更新

1つめに示したミュータブルなオブジェクト更新 (mutableApproach.ts) は、次のように オブジェクトの属性を直接的に変更する方法 でした。

import type { Todo } from "./types.js";

const todo: Todo = {
  name: "Learn TypeScript",
  priority: 3,
  isDone: false,
  deadline: new Date(2024, 9, 11, 9, 45),
};

// ▼▼▼ ここから
const updatedTodo = todo; // ➊
updatedTodo.name = "Learn COBOL"; // ➋
updatedTodo.priority = 1; // ➌
// ▲▲▲ ここまでが着目してほしいところ

// updatedTodo と todo の「参照」は同じ (=同じオブジェクト)
console.log("todo !== updatedTodo ---> ", todo !== updatedTodo);

// todo の内容を確認
console.log("■ todo の内容");
console.log(JSON.stringify(todo, null, 2));

// updatedTodoの内容を確認
console.log("■ updatedTodoの内容");
console.log(JSON.stringify(updatedTodo, null, 2));

実行結果は、次のようになります。

todo !== updatedTodo --->  false
■ todo の内容
{
  "name": "Learn COBOL",
  "priority": 1,
  "isDone": false,
  "deadline": "2024-10-11T00:45:00.000Z"
}
■ updatedTodoの内容
{
  "name": "Learn COBOL",
  "priority": 1,
  "isDone": false,
  "deadline": "2024-10-11T00:45:00.000Z"
}

mutableApproach.ts第11行目 (➊)updatedTodo = todo は、いわゆる 浅いコピー (Shallow Copy) であり、todo参照 (C言語で言えばポインタ、Pythonで言えばオブジェクトIDのようなもの) を、updatedTodo に複製しただけに相当します。

img

そのため、第12行目 (❷)第13行目 (➌)updatedTodo を対象に操作しているように見えますが、実際のところ、それは todo の「参照先」を変更していることにもなります。そのため、実行結果を確認すると、オリジナルの todonameLearn COBOL に変更されています。

このようにオリジナルのデータ (todo) のプロパティを上書きすることから、このようなアプローチは「状態直接操作」や「破壊的更新」、「ミュータブルなオブジェクト更新」のように呼ばれます。

この授業のなかでは、このようなオブジェクトの操作を 「ミュータブルなオブジェクト更新」 のように表現してきます。ミュータブル (Mutable) とは「可変な」「変更可能な」という意味になります。

3.1.2 イミュータブルなオブジェクト更新

前回授業では、上記で示したミュータブルなオブジェクト更新とは別に、次の immutableApproach.ts のように「スプレッド構文」を使ってプロパティを更新した新たなオブジェクトを生成する方法も解説しました。

import type { Todo } from "./types.js";

const todo: Todo = {
  name: "Learn TypeScript",
  priority: 3,
  isDone: false,
  deadline: new Date(2024, 9, 11, 9, 45),
};

// ▼▼▼ ここから
const updatedTodo = {
  ...todo, // スプレッド構文
  name: "Learn COBOL",
  priority: 1,
}; // ➊~➌
// ▲▲▲ ここまでが着目してほしいところ

// updatedTodo と todo の「参照」は違う (=異なるオブジェクト)
console.log("todo !== updatedTodo ---> ", todo !== updatedTodo);

// todo の内容を確認
console.log("■ todo の内容");
console.log(JSON.stringify(todo, null, 2));

// updatedTodo の内容を確認
console.log("■ updatedTodoの内容");
console.log(JSON.stringify(updatedTodo, null, 2));

実行結果は、次のようになります。

todo !== updatedTodo --->  true
■ todo の内容
{
  "name": "Learn TypeScript",
  "priority": 3,
  "isDone": false,
  "deadline": "2024-10-11T00:45:00.000Z"
}
■ updatedTodoの内容
{
  "name": "Learn COBOL",
  "priority": 1,
  "isDone": false,
  "deadline": "2024-10-11T00:45:00.000Z"
}

この方法は、以下の図に示すように、オリジナルのデータ (todo) は変化させずに、別途、プロパティを変更した新しいオブジェクト (updatedTodo) を作成するアプローチであり、「イミュータブルなオブジェクト更新」 のように呼ばれます。新しいオブジェクトが生成された証拠に、第19行目 の出力は true となります。

先ほどの ミュータブル (Mutable) に対して、イミュータブル (Immutable) とは「変更できない」「不変」という意味になります。余談ですが、ミュータブル / イミュータブル などの考え方は「Haskell」や「Scala」などの関数型プログラミングで重要な概念となってきます。

img

immutableApproach.ts第11行目 から 第15行目 の処理は、以下のコードと等価となります。

const updatedTodo = {
  ...todo,
}; // ➊
updatedTodo.name = "Learn COBOL"; // ➋
updatedTodo.priority = 1; // ➌

なお、スプレッド構文は、Pythonにおけるアンパックのようなものとイメージしてください。それでも、イメージがつかみづらいときは「生成AI」を利用してみてください。

(プロンプト例)

JavaScriptにおける「スプレッド構文」のイメージがつかめません。特に「イミュータブルなオブジェクト更新」のために、スプレッド構文を使うという解説を聞いたのですがしっくりきません。具体的なコードを示して分かりやすく解説してください。

3.2 React開発では「イミュータブルなオブジェクト更新」を使用

前回授業で解説したように、Reactを使ったフロントエンド開発において「オブジェクトのプロパティを変更するとき」は、原則として スプレッド構文を利用したイミュータブルなオブジェクト更新 を使用するように意識してください。

なぜならば、React では「オブジェクトの参照が (以前と) 変化しているかどうか」に基づいて 画面表示を更新するかどうか (再レンダリング・再描画するかどうか) を判断・最適化 しているためです。適切に画面が再レンダリングされなければ「内部的にデータが書き換わっていても、利用者が見ているウェブ画面上にはそれが反映されていない」という困った状態になります。

このようなことから、Reactを使った開発では イミュータブルなオブジェクト更新を大原則 とする必要があります。以下は「Reactにおける画面更新の基本的な流れと仕組み」になります。

  1. イミュータブルな操作によって新しいオブジェクトを生成する。
  2. 新しく生成されたオブジェクトは、元のオブジェクトとは 異なる参照(C言語でいえばポインタ、Pythonで言えばオブジェクトID)を持つ。
  3. 新しく生成されたオブジェクトをReactの 状態 (state) にセットする (主に useStateフック を利用)、あるいは、子コンポーネントのProps(プロップス) に渡す。
    • useStateProps については次回以降に詳しく学びます。
  4. Reactは、Pros や state にセットされた オブジェクトの参照の変化を検知 し、画面の再レンダリング (DOM操作) をする。

なお、今回講義では「配列」を学びますが、配列についても「ミュータブルな更新」と「イミュータブルな更新」が存在します。こちらも同様に「イミュータブルな配列更新」をする必要があります (React開発の前提で)。

3.2.1 演習①

イミュータブルなオブジェクト更新によって、期限を Date(2024, 9, 30)、完了フラグを true に変更したオブジェクトを updatedTodo に得るようにプログラムを追記してください。また、実際に結果を確認してください。

import type { Todo } from "./types.js";
import { printTodo } from "./utils/printTodo.js";
import assert from "assert";

const todo: Todo = {
  name: "Learn TypeScript",
  priority: 3,
  isDone: false,
  deadline: new Date(2024, 9, 11, 9, 45),
};

// ここを編集
// const updatedTodo =

// todo と updatedTodo の参照が「異なること」を念のために確認
assert.notEqual(todo, updatedTodo);

// updatedTodo の内容を確認
printTodo(updatedTodo);

4 配列

ここからは 配列 (Array) と、その操作 (特に イミュータブルな配列更新) について学びます。

4.1 導入

前回講義で定義した Todo 型のような「ユーザ定義型の配列」は、Reactにおけるコアなデータとなります。また、その配列に対する操作 (mapfiltersort など) は、Reactによる画面描画と密接に関係してきます。

例えば、Todoアプリに表示される次のような コンポーネント (ウェブ画面構成の部品) は…

img

以下のような Todo型の配列 (Todo[]) に基づいて描画されます。

import type { Todo } from "./types.js";
import { v4 as uuid } from "uuid";

export const initTodos: Todo[] = [
  {
    id: uuid(),
    name: "Reactの勉強 (予習)",
    isDone: false,
    priority: 3,
    deadline: undefined,
  },
  {
    id: uuid(),
    name: "TypeScriptの勉強 (復習)",
    isDone: true,
    priority: 2,
    deadline: undefined,
  },
  {
    id: uuid(),
    name: "基礎物理学3の宿題",
    isDone: false,
    priority: 1,
    deadline: new Date(2024, 10, 11),
  },
  {
    id: uuid(),
    name: "解析2の宿題",
    isDone: true,
    priority: 1,
    deadline: new Date(2024, 10, 16, 17),
  },
];

そして、ウェブ画面上でのボタン押下などのユーザ操作を受けて、todos に要素の追加や削除をしたり、要素のプロパティを変更した updatedTodos を生成し、それをReactに渡すと、React側で再レンダリング (画面の更新) が実行されます。この際、Reactは 変更があった部分のみを検出して効率的に差分だけを再描画 をします。

ただし、ここで注意すべきは…

…ということです。

4.2 型と宣言

まずは、シンプルに数値型や文字列型などのプリミティブデータの「配列」から学んでいきます。TypeScript では、次のように C言語ライクに 配列 (Array) を宣言・初期化します。

// 配列の初期化 (型明示)
const numArr1: number[] = [4649, 3150, 0.5, -1];
const strArr1: string[] = ["M", "D", "E", "知能情報"];

// 配列の初期化 (型推論)
const numArr2 = [4649, 3150, 0.5, -1];
const strArr2 = ["M", "D", "E", "知能情報"];

// 空配列の初期化 (型明示が必ず必要)
const numArr3: number[] = [];
const strArr3: string[] = [];

配列を使用するときは、宣言時に number[] あるいは Array<number> のように「型 (Type)」を指示します。 どちらも同じ意味になりますが、一般には xxx[] の形式が使われます。

TypeScriptの配列は、基本的に同じ型の値 を要素に持つような使い方をします。ただし、次のように型を (number | string)[] とすれば、数値型または文字列型を要素に持つ配列 も可能です。

// 「数値型」または「文字列型」を要素に持つ配列
const arr1: (number | string)[] = ["one", 2, 3];

4.3 参考: 要素の追加と削除 (ミュータブルな配列操作)

React開発では基本的に使用することはありませんが、ミュータブルな配列要素の「追加」と「削除」の例を示しておきます。このプログラムでは、配列が持つ pushunshiftsplicepop などのメソッドを使っていますが、これらを本授業の範囲で使用する場面は (おそらく) ありません

const numArr: number[] = [10, 11, 12, 13];
console.log("初期状態 => " + numArr);

numArr.push(14); // 末尾に要素を追加
numArr.unshift(9); // 先頭に要素を追加
console.log("先頭と末尾に要素を追加した後 => " + numArr);

// 2番目に要素(10.5)を挿入 ゼロオリジンに注意
numArr.splice(2, 0, 10.5);
console.log("2番目の位置に要素を挿入した後 => " + numArr);

numArr.pop(); // 末尾の要素を削除
console.log("末尾の要素を削除した後 => " + numArr);

// 4番目の要素を削除
numArr.splice(4, 1);
console.log("4番目の位置の要素を削除した後 => " + numArr);

実行結果は、次のようになります。

初期状態 => 10,11,12,13
先頭と末尾に要素を追加した後 => 9,10,11,12,13,14
2番目の位置に要素を挿入した後 => 9,10,10.5,11,12,13,14
末尾の要素を削除した後 => 9,10,10.5,11,12,13
4番目の位置の要素を削除した後 => 9,10,10.5,11,13

4.4 要素の追加 (イミュータブルな変更)

React開発では、pushpopsplice などのメソッドを使用せずに、配列に対してイミュータブルな操作をすること が要求されます。

配列に対する 要素の追加 に関しては、次のように「スプレッド構文」を利用してイミュータブルな操作 (=元の配列には変更を加えずに、「変更を適用した新しい配列」を生成すること) が可能です。

const numArr: number[] = [10, 11, 12, 13];
console.log("初期状態 => " + numArr);

// 末尾に要素を追加
const addedToEnd = [...numArr, 14]; // スプレッド構文
console.log("末尾に要素を追加 => " + addedToEnd);

// 先頭に要素を追加
const addedToStart = [9, ...numArr]; // スプレッド構文
console.log("先頭に要素を追加 => " + addedToStart);

// n番目の位置に要素 (10.5) を挿入
const n = 2;
const insertedAtN = [...numArr.slice(0, n), 10.5, ...numArr.slice(n)];
console.log(`${n}番目の位置に要素を挿入 => ` + insertedAtN);

第14行目 では「配列の途中位置に要素を追加する例」を示していますが、実際の開発では、末尾か先頭に要素を追加したうえで sort() メソッドを使って並び替えすること が多いです。よって、特に覚えるべき処理は、スプレッド構文を利用して「先頭」または「末尾」に要素を追加する処理 になります。

配列要素の「削除」については、後ほど解説する filter() メソッドを利用します。

spliceslice の違い

配列操作の splice メソッド と slice メソッドは機能と使用方法が異なるので注意してください。興味がある人は、生成AIを使用して調べてみてください。

(プロンプト例)

JavaScriptの配列の splice メソッド と slice メソッドの違いをミュータブル、イミュータブルの観点から説明してください。

4.5 map による要素の更新

モダンTypeScriptにおいて イミュータブルに配列要素を更新 するためには、一般に 「アロー関数」と「map() メソッド」 を組み合わせて使用します。これは、プログラミング1 (Python) で学んだ「ラムダ式map関数組み合わせ」に相当するものです。

例えば、「学年」を表す数値型の配列 grade から、HTML用に整形した文字列の配列 gradeListItems を得るためのイミュータブルな操作は、次のように記述できます。

const grades: number[] = [1, 2, 3, 4, 5]; // 学年

// ▼▼▼ ここから
const gradeListItems = grades.map((grade: number): string => {
  return `<li>${grade}年</li>`;
});
// ▲▲▲ ここまでが着目してほしいところ

console.log(grades);
console.log(gradeListItems);

実行結果は、次のようになります。配列の要素が 1 から '<li>1年</li>' のように変換されていること (mapされていること、射影されていること) が確認できると思います (特定の要素だけに変換を適用する方法については後述します)。

[ 1, 2, 3, 4, 5 ]
[
  '<li>1年</li>',
  '<li>2年</li>',
  '<li>3年</li>',
  '<li>4年</li>',
  '<li>5年</li>'
]

処理の本質は 第04行目から第06行目 になりますが初見で読み解くことは かなり難しい と思います。これを読み解き、理解するために「レガシーな手続き型スタイルの配列操作」から、上記のような「モダンな宣言型スタイルの配列操作」に書き換える例を以下に示します。

4.5.1 第1形態 : レガシーな手続き型スタイル

まず、従来型の書き方をすれば、以下のようになります。既に皆さんは「C言語」を学んできているので、十分に読み解くことができると思います。

const grades: number[] = [1, 2, 3, 4, 5];
const gradeListItems: string[] = []; // 要素が空の配列
for (let i = 0; i < grades.length; i++) {
  const listItem = `<li>${grades[i]}年</li>`;
  gradeListItems.push(listItem); // 要素を追加
}

ここでは簡略化のために export {};console.log(...) の部分は省略しています。実際に動作確認する際には、それらを追加して実行してください。

なお、配列は、length プロパティを持ち、それを通して配列の要素数を得ることができます。例えば、const grades = [1, 2, 3, 4];grades.length4 となります。

4.5.2 第2形態 : for…of に書き換え

forループ変数を「配列のインデックスi」から「配列の要素そのものgrade」に変えました。第03行目第04行目 を書き換えていますが、実行結果は先ほどと同じく期待する出力を得ることができます。

const grades: number[] = [1, 2, 3, 4, 5];
const gradeListItems: string[] = [];
for (const grade of grades) { // ■■ ここを書き換えた ■■ 
  const listItem = `<li>${grade}年</li>`; // ■■ ここを書き換えた ■■ 
  gradeListItems.push(listItem);
}

なお、for (const grade of grades) によって、ループ毎に変数 grade のなかには 123… という値が格納されます。もし、第01行目const grades = [9, 1, 5] としていれば、ループ変数である grade には、ループ毎に 915 という値が格納されてfor文の内部の処理が実行されます。

Python で書けば for grade in grades: ですね (参照)。

4.5.3 第3形態 : 変換処理の関数化

数値型の値 (例えば 3 ) を、整形された文字列型の値 (例えば "<li>3年</li>") に変換するための処理を func として分離しました。また、第08行目 で、それを func(grade) にように呼び出しています。

function func(grade: number): string {
  return `<li>${grade}年</li>`;
}

const grades: number[] = [1, 2, 3, 4, 5];
const gradeListItems: string[] = [];
for (const grade of grades) {
  gradeListItems.push(func(grade));
}

4.5.4 第4形態 : アロー関数化

「function関数」を「アロー関数」に書き換えました。第01行目 だけを書き換えました。

const func = (grade: number): string => { // ■■ アロー関数化 ■■ 
  return `<li>${grade}年</li>`;
};

const grades: number[] = [1, 2, 3, 4, 5];
const gradeListItems: string[] = [];
for (const grade of grades) {
  gradeListItems.push(func(grade));
}

4.5.5 第5形態 : mapメソッドの利用

for を使って実行していた処理を、map を使った処理に書き換えました。第4形態の 第06行目 から 第09行目 までの処理が、ここでは 第05行目 の「1文だけ」でスッキリと記述できています。

const func = (grade: number): string => {
  return `<li>${grade}年</li>`;
};

const grades: number[] = [1, 2, 3, 4, 5];
const gradeListItems = grades.map(func); // ■■ 注目 ■■ 

map()元の配列を変更せず、配列の各要素に func を適用した「新しい配列」を作成して戻り値とします。これは、React開発で重要となってくる特性なので覚えておいてください。

4.5.6 第6形態 : mapの引数に直接的にアロー関数を記述

第5形態では、変数 func を経由して、mapメソッドの引数に「整形の処理」を与えていました。それを、ここではアロー関数の形式で 直接的 に与えるように書き換えました。

const grades: number[] = [1, 2, 3, 4, 5];
const gradeListItems = grades.map((grade: number): string => {
  return `<li>${grade}年</li>`;
});

ここでは、整形処理をする関数に名前を与える必要がないこともうれしいですね(適切な名前を考えるのは面倒ですから…)。

ここまでで、最初に示したモダンスタイルな配列処理に変換が完了しました。

4.5.7 第7形態 (省略形)

アロー関数のなかの処理が return ...; の1文だけで構成できる場合は、以下のように「波括弧」と「retuen」を省略して書くこともできます。

const grades: number[] = [1, 2, 3, 4, 5];
const gradeListItems = grades.map((grade: number): string => `<li>${grade}年</li>`
);

4.5.8 第8形態 (型推論を利用)

型推論ができる場合は、型を省略することができます。

const grades: number[] = [1, 2, 3, 4, 5];
const gradeListItems = grades.map((grade) => `<li>${grade}年</li>`);

最終的には、次のように記述できます。

const grades: number[] = [1, 2, 3, 4, 5];
const gradeListItems = grades.map((grade) => `<li>${grade}年</li>`);
console.log(grades);
console.log(gradeListItems);

実行結果は、次のようになります。

[ 1, 2, 3, 4, 5 ]
[
  '<li>1年</li>',
  '<li>2年</li>',
  '<li>3年</li>',
  '<li>4年</li>',
  '<li>5年</li>'
]

4.5.9 演習②

期待する結果が得られるように、次のプログラムを完成させてください。ここでは優先度 (優先順位)「1」が「★★★」で、優先度 (優先順位)「3」が「★」になる点に注意してください。

const priorities = [3, 1, 2, 1]; // 1〜3の値が格納された配列

// ここの処理を完成させる
// const formattedPriorities = 

console.log(priorities);
console.log(formattedPriorities);

期待する結果

[ 3, 1, 2, 1 ]
[ '★', '★★★', '★★', '★★★' ]

4.6 配列のインデックス番号の参照

次のプログラムの 第02行目 のように map に第2引数を設定すると、その変数には ゼロオリジンのイデックス番号 が格納され、map内の処理で参照することができます。

const arr = ["Python", "C言語", "TypeScript", "C#"];
const arr2 = arr.map((value, index) => { // index の設定
  return `${index + 1}: ${value}`; // index の参照 (読み取り)
});
console.log(arr2);

実行結果は以下のようになります。

[ '1: Python', '2: C言語', '3: TypeScript', '4: C#' ]

5 オブジェクト配列のmap操作

次にオブジェクトを要素とする配列の「map操作」について考えていきます。

5.1 準備 (型定義の変更)

ファイルが増えてきたので、仕切り直しします。次のように、src フォルダのなかに pipeline というサブフォルダを作成してください。

img

そのなかに、以下のように数値型の id というプロパティを新た追加した Todo 型 (=ユーザ定義のオブジェクト型) を定義した types.ts というファイルを作成してください。

export type Todo = {
  id: number;
  name: string;
  priority: number;
  isDone: boolean;
  deadline: Date;
};

5.2 オブジェクト型の配列の初期化

ユーザ定義のオブジェクト型を要素に持った 配列の初期化 は次のように行ないます。src/pipeline フォルダのなかに initTodos.ts というファイルを新規作成して以下の内容を記述してください。

import type { Todo } from "./types.ts";

export const initTodos: Todo[] = [
  {
    id: 1,
    name: "React予習(YouTube)",
    isDone: false,
    priority: 1,
    deadline: new Date(2024, 9, 24, 9, 0),
  },
  {
    id: 2,
    name: "TypeScriptの復習",
    isDone: true,
    priority: 2,
    deadline: new Date(2024, 9, 30),
  },
  {
    id: 3,
    name: "基礎物理学3の宿題",
    isDone: false,
    priority: 1,
    deadline: new Date(2024, 9, 20, 23, 59),
  },
  {
    id: 4,
    name: "知識科学概論の宿題",
    isDone: true,
    priority: 3,
    deadline: new Date(2024, 9, 27),
  },
];

numberstring などのプリミティブ型の配列と同様に、オブジェクト型の配列についても型付きの変数の宣言は Todo[] あるいは Array<Todo> のようにします。また、初期化は、それに続けて =[{...},{...},{...}] の形式で行ないます。

(プロンプト例)

TypeScriptにおける「プリミティブ型」とはなんですか。

5.3 オブジェクト型の配列に対するmapの適用

ここでは、mapを使って「Todo型のデータ」を「整形された文字列」に変換する例を示します。src/pipeline のなかに map01.ts を新規作成して、以下のコードを貼付けてください。

ここでは、特に 第07行目 に着目して読解してください。

import type { Todo } from "./types.js";
import { initTodos } from "./initTodos.js";
import dayjs from "dayjs";

const dtFmt = "YYYY/MM/DD HH:mm";
const formattedTodos: string[] = initTodos.map((t: Todo) => {
  const str =
    `<li>[${t.id}] ${t.name} 優先度${t.priority} ` +
    `(期限${dayjs(t.deadline).format(dtFmt)})` +
    (t.isDone ? "【済】" : "") +
    "</li>";
  return str;
});

console.log(formattedTodos);

実行結果は、次のようになります。実際に実行して確認してください。

[
  '<li>[1] React予習(YouTube) 優先度1 (期限2024/10/24 09:00)</li>',
  '<li>[2] TypeScriptの復習 優先度2 (期限2024/10/30 00:00)【済】</li>',
  '<li>[3] 基礎物理学3の宿題 優先度1 (期限2024/10/20 23:59)</li>',
  '<li>[4] 知識科学概論の宿題 優先度3 (期限2024/10/27 00:00)【済】</li>'
]

この例では、Todo型の配列の要素を受け取る仮引数を t という名前にしていますが (第06行目を参照)、これは自由に名前を付けることが可能です。このケースでは todo のような名前にするのが一般的です。

プログラミング1の授業でも紹介しましたが、以下のようにVSCodeの機能で 変数名の一括変更 ができます。通常の一括置換では コメント内の文字列 なども影響を受けますが、この機能では 変数のスコープ (有効範囲) を識別して適切な置換が実行されます。

img

実際に、VSCode の変数名の変更機能を使って、ttodo に変更してください。

5.4 任意の配列要素のプロパティの変更

オブジェクト配列のなかの「任意の配列要素」の「任意のプロパティ」を イミュータブルに変更する方法 (=その変更が React に適切に検知されるようにする方法) を解説します。

ここでは、id4 である Todo (知識科学概論の宿題) の isDone プロパティを false に変更するような操作を例に解説したいと思います。

5.4.1 NG: ミュータブルな操作

まずはNGな変更操作から確認していきます。次のプログラムの 第11行目 から 第16行目 までの処理は、React開発ではNGなミュータブルな操作 となります。

コンソール出力から確認できるように内部データとしては、id4 の「知識科学概論の宿題」の isDonefalse に変更されていますが、このような操作ではReactで管理・描画されるウェブ画面上の表示は変更されません (Reactでのハマりポイントです)。

import { initTodos } from "./initTodos.js";

console.log("プロパティ変更前");
console.log(JSON.stringify(initTodos, null, 2));
// React の useState の更新関数
// setTodos(initTodos);

// NGなプロパティ変更
const targetId = 4; // isDone を false に戻す対象
for (const todo of initTodos) {
  if (todo.id === targetId) {
    todo.isDone = false;
    break;
  }
}
console.log("プロパティ変更後");
console.log(JSON.stringify(initTodos, null, 2));
// React の useState の更新関数
// setTodos(initTodos);

プログラムのなかでコメントアウトしている setTodos(initTodos) は、実際に React で状態管理をするときに使用するものです。このような関数を通してReactにデータを渡します。現時点では理解する必要はありません。

5.4.2 OK: イミュータブルな操作

Reactで画面の再レンダリングがトリガーされるようにするためには、次のように map を使って 配列を新しく作成する (=配列の参照を新しくすることでReactに変更を検知してもらう) と共に、スプレッド構文で操作対象のオブジェクトも新しく作成する (=オブジェクトの参照を新しくすることでReactに変更を検知してもらう) 必要があります。

import { initTodos } from "./initTodos.js";

console.log("プロパティ変更前");
console.log(JSON.stringify(initTodos, null, 2));
// React の useState の更新関数
// setTodos(initTodos);

// 推奨されるプロパティ変更
const targetId = 4;
const updatedTodos = initTodos.map((todo) => { // mapメソッドを利用
  if (todo.id === targetId) {
    return { ...todo, isDone: false }; // スプレッド構文を利用
  } else {
    return todo;
  }
});
console.log("プロパティ変更後");
console.log(JSON.stringify(updatedTodos, null, 2));
// React の useState の更新関数
// setTodos(updatedTodos);

また、上記のプログラムは条件演算子(三項演算子)を使用して、以下のように記述することができます。

import { initTodos } from "./initTodos.js";

console.log("プロパティ変更前");
console.log(JSON.stringify(initTodos, null, 2));
// React の useState の更新関数
// setTodos(initTodos);

const targetId = 4;
const updatedTodos = initTodos.map((todo) => {
  return todo.id === targetId ? { ...todo, isDone: false } : todo; // 条件演算子
});
console.log("プロパティ変更後");
console.log(JSON.stringify(updatedTodos, null, 2));
// React の useState の更新関数
// setTodos(updatedTodos);

5.4.3 演習③

targetId で指定された Todo について name のプロパティを イミュータブルに変更 するように、次のプログラムを完成させてください。

import { Todo } from "./types";
import { initTodos } from "./initTodos";

const targetId = 3;
const newName = "電気電子回路1の課題";
const updatedTodos: Todo[] = [];

console.log(JSON.stringify(updatedTodos, null, 2));

6 オブジェクト配列のfilter操作

配列の map メソッドは 引数として関数を受け取り 、配列の各要素にその関数を適用した「新しい配列」を作成して戻り値としました。これに対して filter メソッドは map と同様に引数として関数を受け取りますが、こちらはその関数に配列の各要素を適用した結果が true になる要素 から構成される「新しい配列」を作成して戻り値とします。

img

6.1 filterメソッドの基本的な使用法

filter メソッドは、配列の各要素に対して条件(=真偽値を戻り値とする関数)を適用し、その条件を満たす要素だけを含む「新しい配列」を返す機能を持っています。map と同様に元の配列は影響を受けません。

例えば、次のように使用します。

const numArr: number[] = [1, 2, 3, 4, 5, 6];
const oddArr: number[] = numArr.filter((num) => {
  return num % 2 === 1; // 奇数か? ture or false
});
console.log(`numArr = ${numArr}`);
console.log(`oddArr = ${oddArr}`);

第02行目から第04行目const oddArr = numArr.filter(num => num % 2 === 1); のような 省略表記 もできます。

実行結果は次のようになります。アロー関数で与えた関数による評価結果が true の要素だけが出力されていることが確認できます。

numArr = 1,2,3,4,5,6
oddArr = 1,3,5

なお、numArr.filter(...) の戻り値が [true, false, true, false, true, false] になるわけではないので注意してください (逆に、このような配列を得たいときは map メソッド を使用してください)。

6.2 オブジェクト型の配列に対するfilterの適用

Todo 型の配列であるinitTodos から「isDone false の要素だけを抽出した配列」を得る処理は (=これは isDonetrue の要素を「削除」した配列を得る処理 と同義) は、filter を使って、次のように実装できます。

import { initTodos } from "./initTodos.js";

const updatedTodos = initTodos.filter((todo) => !todo.isDone);
console.log("未完了Todoの一覧");
console.log(JSON.stringify(updatedTodos, null, 2));

推測がつくと思いますが、上記の 第03行目!todo.isDone! は 真偽値の 論理否定演算子 です。つまり、todo.isDonefalse のとき、!todo.isDonetrue となりフィルタを通過します。

また「任意の id を持った要素を配列から削除する処理」も filter を利用して、次のように実装が可能です。

import { initTodos } from "./initTodos.js";

const targetId = 2; // 削除対象のTodoのID
const updatedTodos = initTodos.filter((todo) => todo.id !== targetId);
console.log("削除処理後のTodoの一覧");
console.log(JSON.stringify(updatedTodos, null, 2));

6.2.1 演習④

Todoオブジェクトの配列のなかで「未完了」かつ「期日 (deadline) が today を過ぎている」に一致する要素を overdueTodos に得るように、次のプログラムを完成させてください。

なお、適切な動作検証ができるように initTodos を適当に変更してください。

import type { Todo } from "./types.js";
import { initTodos } from "./initTodos.js";

const today = new Date(2024, 9, 22);
const overdueTodos: Todo[] = []; // 主にここを書き換え
console.log("期日を過ぎている未完了Todoの一覧");
console.log(JSON.stringify(overdueTodos, null, 2));

7 オブジェクト配列のsort操作

イミュータブルに配列のソート (並び替え) を行なうためには スプレッド構文sort メソッドを組みあわせて使用します。

7.1 ソートキーを指定したオブジェクト配列の並び替え

オブジェクト配列の並び替えるには、sortメソッドの引数に ソートに使用するプロパティを使った「比較関数」 を与える必要があります。例えば、Todo を 優先度 (優先順位) の「昇順」にソートする処理は次のように実装できます。

import { initTodos } from "./initTodos.js";

const sortedTodos = [...initTodos].sort((a, b) => {
  return a.priority - b.priority;
});

console.log(JSON.stringify(initTodos, null, 2));
console.log(JSON.stringify(sortedTodos, null, 2));

実際に実行して、結果を確認してください (特に iniTodos の並び順は影響を受けていないことを確認してください)。

また、第03行目[...initTodos].sortinitTodos.sort に変更すると ミュータブルな操作になってしまうこと (つまり initTodos が変更されてしまうこと) を実際に確認してください。

sort の引数には、配列の要素となっている型 (ここでは Todo 型) の2つの引数 (通常 ab という名前にすることが多い) を受け取り「負数 (通常は -1)」、「0」、「正数 (通常は 1)」を返す関数を与えます。この関数は・・・

一般に、この関数は「比較関数」や「カスタム比較関数」と呼ばれます。

7.1.1 演習

Todo を 優先度 (優先順位) の「降順」にソートする処理を実装してください。また、動作を確認してください。

7.2 複数のソートキーを使ったオブジェクト配列の並び替え

第1ソートキーを isDone (未完了を先に表示)、第2ソートキーを deadline として並び替えたいときは、以下のように実装します。

import type { Todo } from "./types.js";
import { initTodos } from "./initTodos.js";

const sortedTodos: Todo[] = [...initTodos].sort((a, b) => {
  if (a.isDone !== b.isDone) {
    return a.isDone ? 1 : -1;
  } else {
    return a.deadline.getTime() - b.deadline.getTime();
  }
});

console.log(JSON.stringify(initTodos, null, 2));
console.log(JSON.stringify(sortedTodos, null, 2));

第05-06行目 で「abisDone違う」ならば isDonefalse のほうを「前」に配置するようにしています。また、第07-08行目 で「abisDone同じ」ならば deadline が過去のほう (古いほう) のほうを「前」に配置するようにしています。

7.2.1 演習

第1ソートキーを「優先度」、第2ソートキーを「期限」として Todos を並び替えるように実装してください。また、動作を確認してください (必要に応じて initTodos に編集を加えてください)。

8 メソッドチェーン (map・filter・sortの組み合わせ)

mapfiltersort連結して使用すること も可能なので知っておいてください (実務では頻繁に利用されます)。

例えば、filter未完了のTodoだけを抽出し、sort優先度順に並べ替えて、map整形出力するという処理は次のように記述できます。このようにメソッドを連結する技法を メソッドチェーン や メソッドチェイニング のように表現します。

なお、sort がメソッドチェーンの開始点でなければ、スプレッド構文を使用せずとも initTodos に対するイミュータブルな操作となります。もし、sort をメソッドチェーンの開始点にする場合は [...initTodos].sort() のようにスプレッド構文を組み合わせて使用 してください。

import dayjs from "dayjs";
import { initTodos } from "./initTodos.js";
const dtFmt = "YYYY/MM/DD HH:mm";

const listItemsTodos: string[] = initTodos
  .filter((todo) => !todo.isDone)
  .sort((a, b) => a.priority - b.priority)
  .map(
    (todo) =>
      `<li>優先度[${todo.priority}] ${todo.name}` +
      `...期限${dayjs(todo.deadline).format(dtFmt)}</li>`
  );
console.log(JSON.stringify(initTodos, null, 2)); // 変更操作の影響を受けていない
console.log(JSON.stringify(listItemsTodos, null, 2));

実際に上記のプログラムを実行して、出力を確認してください (必要に応じて initTodos に編集を加えてください)。

8.1 関数化

mapfiltersort などの配列操作メソッドは、各種処理をコンパクトに記述することが可能です。ただし、同じ配列操作を2回以上行なうときは、それらの処理を関数化することが推奨されます。

以下のプログラムは…

…の実装例となります。

import dayjs from "dayjs";
import type { Todo } from "./types.js";
import { initTodos } from "./initTodos.js";
const dtFmt = "YYYY/MM/DD HH:mm";

const deleteTodoById = (todos: Todo[], id: number): Todo[] => {
  if (!Number.isInteger(id) || id < 0) {
    return todos;
  }
  return todos.filter((todo) => todo.id !== id);
};

const stringifyTodos = (todos: Todo[]): string[] =>
  todos.map(
    (todo) =>
      `id=${todo.id} ${todo.name} ` +
      `期限 ${dayjs(todo.deadline).format(dtFmt)}`
  );

const todo = [...initTodos];
const updatedTodos = deleteTodoById(todo, 2);

console.log("Before:", stringifyTodos(todo));
console.log("After:", stringifyTodos(updatedTodos));

慣れるまでは、上記のような関数処理を読み解くことも、記述することも大変かと思います。しかし、React開発では、状態(state)の更新や、コンポーネントの再レンダリングを効率的に行うために 頻繁に配列操作メソッドが使用されます

はじめのうちは超難しく感じるかもしれませんが、継続的な学習と実践により、徐々に理解が深まり、効率的なコードを書けるようになります。頑張ってください (1回で理解できることは、まず、あ・り・ま・せ・ん。1日か2日の間隔を空けて3回ほどトライすると、ぼんやりと分かってくるようになります)。

(プロンプト例)

React開発のための準備として、TypeScriptにおける配列操作メソッド (mapfiltersort) の読解と記述について十分に慣れておきたいと思っています。オブジェクトの配列を対象とした配列操作メソッドに関する練習問題と解答例を作成してください。まずは、初歩の初歩、超簡単なものからお願いします。

9 宿題

次回から 本格的なReact開発 に突入します🎉

予習として以下の動画 (約1時間) を視聴してきてほしいです。動画を事前視聴しておくことで (=「何が分からないか」を自覚しておくことで) 授業での理解が格段に進みます。