【デイトラ学習記録】TypeScriptの基礎知識とインストール〜TODOアプリを作ろう!

今回は、TypeScriptについて学習していきます。

TypeScriptとは?

  • TypeScriptとは、JavaScriptに型の仕組みを加えたプログラミング言語です。
  • Googleの標準言語として使用されるなど、フロンドエンド開発に欠かせない言語

静的型付け言語動的型付け言語

動的型付け言語 (JavaScript)

JSは動的型付け言語のため、実行時に型関連のエラーがでやすい。

JavaScript
let value = 42; // 数値型
value = "Hello"; // 文字列型(型が変わる)

静的型付け言語(TypeScript)

TypeScriptは、JavaScriptに型付けの仕組みを追加した静的型付け言語です。

TypeScript
let value: number = 42; // 数値型
value = "Hello"; // エラー: 型が一致しない
  • 型を決めることで、エラーやバグを防ぐことができる
  • 開発効率があがる
  • チーム開発しやすくなる

TypeScriptの導入

  1. tsconfig.json の用意
  2. npm install
  3. webpack.config.js に設定を追加
  4. index.ts ファイルを用意して実行

TypeScriptを導入することで、古いブラウザ対応のwebpackのBabelは不要になります。
今回は webpack + TailwindCSS + TypeScript の環境を構築しました。

環境は以下のようなパターンがあります。

  • TypeScriptのみ
    • npx tsc src/js/index.ts コマンドラインでコンパイル可能
  • webpack + TypeScript
  • webpack + Babel + TypeScript (元々Babelで運用していたところに、後からTypeScriptの導入)

Node.jsの基礎知識とインストール

  • JavaScriptはブラウザ上で動く言語
  • PHPなどは、ブラウザではなく、コンピュータのOS上で動くサーバーサイド言語
  • Node.jsは、 JavaScriptをサーバーサイドで動かす言語

Node.jsのできること

  • 非同期処理
  • モジュールシステム
  • パッケージマネージャー
  • コマンラインツールの作成

TypeScriptを用いてTODOアプリを実装する

型定義

TypeScript
import { getInputElementById } from "./utils/dom";

/**
 * Todoの型宣言
 */
export type Todo = {
  name: string;
  person: string;
  deadline: string;
};

/**
 * DOMのinput要素から新しいTODOを取得する
 * @returns Todo
 */
export const getNewTodo = (): Todo => ({
  name: getInputElementById("new-todo-name").value,
  person: getInputElementById("new-person").value,
  deadline: getInputElementById("new-deadline").value,
});

id属性で要素を取得する汎用関数を切り出す

JavaScritptの場合、nullチェックが必要になる。

JavaScript
const buttonRegister = document.getElementById("#button-register");

if (buttonRegister) {
  buttonRegister.addEventListener("click", () => {
    console.log("click");
  });
}
JavaScript
const buttonRegister = document.getElementById("#button-register");

buttonRegister?.addEventListener("click", () => {
  console.log("click");
});
JavaScript
const buttonRegister = document.getElementById("#button-register")!;

buttonRegister.addEventListener("click", () => {
  console.log("click");
});

id属性の取得とnullチェックの機能を関数に切り出す。
実際の現場でも、src/js/utils の配下に、汎用関数を定義することが多い

TypeScript
/**
 * id属性からHTML要素を取得する
 * @param id 
 * @returns HTMLElement
 */
export const getElementById = (id : string) : HTMLElement => {
  const element = document.getElementById(id);
  if ( !element ) {
    throw new Error(`${id} not found`);
  }
  return element;
};

input用の要素取得の関数を切り出す

TypeScript

/**
 * id属性からHTMLInputElement要素を取得する
 * @param id 
 * @returns HTMLInputElement
 */
export const getInputElementById = (id : string) : HTMLInputElement => {
  const element = document.getElementById(id);
  
  if ( !element ) {
    throw new Error(`${id} not found`);
  }

   return element as HTMLInputElement;
};

型キャストの概念

型キャストとは、ある型の変数を別の型に変換する操作のことです。TypeScriptでは、特定の型の変数を別の型として扱いたい場合に型キャストを使用します。

例えば、getInputElementById 関数では、document.getElementById(id) が返す要素をHTMLInputElementとして扱いたいため、以下のように型キャストを行っています。

return element as HTMLInputElement;

これにより、TypeScriptはelementがHTMLInputElementであると認識し、HTMLInputElement特有のプロパティやメソッドにアクセスできるようになります。

HTML要素を作る汎用関数を切り出す

TypeScript
/**
 * HTML要素を生成する
 *
 * @param elementName 要素名
 * @param textContent 要素のテキストコンテント
 * @param className 要素のクラス属性
 * @returns HTMLElement
 */
export const createElement = (
  elementName: string,
  textContent?: string,
  className?: string
): HTMLElement => {
  const element = document.createElement(elementName);

  if (typeof textContent !== "undefined") {
    element.textContent = textContent;
  }

  if (typeof className !== "undefined") {
    element.className = className;
  }

  return element;
};

TODO一覧を表示するロジックを作る

TypeScript
/**
 * DOMにTODO一覧を表示する
 */
export const appendTodoList = (todoList: Todo[]): void => {
  todoList.forEach((todo) => {
    const nameTd = createElement(
      "td",
      todo.name,
      "w-[30%] border border-gray-400 px-2 py-2"
    );
    const personTd = createElement(
      "td",
      todo.person,
      "w-[30%] border border-gray-400 px-2 py-2"
    );
    const deadlineTd = createElement(
      "td",
      todo.deadline,
      "w-[30%] border border-gray-400 px-2 py-2"
    );
    const tr = createElement("tr");
    tr.appendChild(nameTd);
    tr.appendChild(personTd);
    tr.appendChild(deadlineTd);
    const tbody = getInputElementById("todo-list");
    tbody.appendChild(tr);
  });
};

appendChildは上書きではなく追加なので重複するため解消する

TypeScript
/**
 * DOMからTODO一覧を削除する
 *
 *  @returns void
 */
const removeTodoListElement = (): void => {
  const tbody = getInputElementById("todo-list");
  while (tbody.firstChild) {
    tbody.firstChild.remove();
  }
};
TypeScript
import "../css/output.css";
import {
  appendTodoList,
  getNewTodo,
  removeTodoListElement,
  Todo,
} from "./todo";
import { getElementById } from "./utils/dom";

let todoList: Todo[] = [];

const buttonRegister = getElementById("button-register")!;

buttonRegister.addEventListener("click", () => {
  // 新しいTODOからDOMを取得する
  todoList.push(getNewTodo());

  // TODO一覧を表示する
  removeTodoListElement(); // tbodyの中身を空にしておく
  appendTodoList(todoList); // TODOの一覧をDOMに追加する
});

スプレッド構文

TypeScript
// pushメソッドは、配列の末尾に要素を追加します。
let todoList: Todo[] = [];
todoList.push(getNewTodo());



// スプレッド構文は、既存の配列の要素を新しい配列に展開し、新しい要素を追加します。
let todoList: Todo[] = [];
todoList = [...todoList, getNewTodo()];

スプレッド構文(…)は、既存の配列の要素を新しい配列に展開し、新しい要素を追加します。
スプレッド構文を使うと、新しい配列が作成されるため、元の配列を変更せずに新しい配列を作成することができます。これは、イミュータブルなデータ操作を行いたい場合に便利です

使用例

TypeScript
let array1 = [1, 2, 3];
let array2 = [...array1, 4, 5];
console.log(array2);
// [1, 2, 3, 4, 5]

関数型プログラミング

  • デバックしやすく、コードが置いやすい
  • 各処理を関数に切り出して書いていく
  • コードを置いやすくデバックしやすくなる
  • モダンな開発では主流になっている

削除ボタンの追加とTypeScriptのスコープの概念

  • TypeScriptでは、変数定義が自動的にスコープ化される。
  • そのため、グローバルに定義しても、外部からアクセスできなくなる
  • なので、変数の内容を上書きたい場合には、同じファイルの中で上書き処理を関数に切り出して、別のファイルで使用する
index.ts
let todoList: Todo[] = [];

const buttonRegister = getElementById("button-register")!;
buttonRegister.addEventListener("click", () => {
  // 新しいTODOからDOMを取得する
  todoList = [...todoList, getNewTodo()];

  // TODO一覧を表示する
  removeTodoListElement();
  appendTodoList(todoList, deleteTodo);
});

/**
 * TODOを削除する
 * @param id
 * @returns void
 */
const deleteTodo = (id: number) => {
  todoList = todoList.filter((todo) => todo.id !== id);
  removeTodoListElement();
  appendTodoList(todoList, deleteTodo);
};
todo.ts
/**
 * DOMにTODO一覧を表示する
 */
export const appendTodoList = (todoList: Todo[], deleteTodo: (id: number) => void) => {
    // 削除ボタンを追加
    const deleteButton = createElement("button", "削除", "border bg-red p-1");
    deleteButton.addEventListener("click", () => deleteTodo(todo.id));
    // deleteButton.addEventListener("click", () => {
    //   todoList = todoList.filter((_todo) => _todo.id !== todo.id);
    //   removeTodoListElement();
    //   appendTodoList(todoList, deleteTodo);
    // });
  });
};

検索機能の追加

todoList の変更タイミングは以下の2箇所

  • 登録ボタン押下時
  • deleteTodo() の実行時

検索絞り込みは、appendTodoList() で、DOMを生成する際に、フィルターワードを使ってフィルタリングする。
そのために、index.ts の中ではフィルターinputの変更監視とフィルターワードの取得を行う。

index.ts
let todoList: Todo[] = [];
let filterWord: string = "";

// 登録ボタン押下時の処理
const buttonRegister = getElementById("button-register")!;
buttonRegister.addEventListener("click", () => {
  // 新しいTODOからDOMを取得する
  todoList = [...todoList, getNewTodo()];

  // TODO一覧を表示する
  removeTodoListElement();
  appendTodoList(todoList, filterWord, deleteTodo);
});

// 絞り込み入力時の処理
const filterInput = getInputElementById("filter");
filterInput.addEventListener("input", () => {
  filterWord = filterInput.value;
  removeTodoListElement();
  appendTodoList(todoList, filterWord, deleteTodo);
});

/**
 * TODOを削除する
 * @param id
 * @returns void
 */
const deleteTodo = (id: number) => {
  todoList = todoList.filter((todo) => todo.id !== id);
  removeTodoListElement();
  appendTodoList(todoList, filterWord, deleteTodo);
};

変更監視と、ワードを使って、appendTodoList で検索ワードを使えるようにする
あくまで特定のワードでフィルタしてるだけど、元のtodoListは書き換えていないので、データは保管されている状態をキープできる。

todo.ts
/**
 * DOMにTODO一覧を表示する
 */
export const appendTodoList = (
  _todoList: Todo[],
  _filterWord: string,
  deleteTodo: (id: number) => void
) => {
  _todoList
    // メソッドチェーンで、_filterWordを使って絞り込む
    .filter((todo) => todo.name.includes(_filterWord) || todo.person.includes(_filterWord))
    .forEach((todo) => {
      const nameTd = createElement("td", todo.name, "border border-gray-400 px-2 py-2");
      const personTd = createElement("td", todo.person, "border border-gray-400 px-2 py-2");
      const deadlineTd = createElement("td", todo.deadline, "border border-gray-400 px-2 py-2");

      // 削除ボタンを追加
      const deleteButton = createElement("button", "削除", "border bg-red p-1");
      deleteButton.addEventListener("click", () => deleteTodo(todo.id));
      const deleteButtonTd = createElement("td", undefined, "border border-gray-400 px-2 py-2");
      deleteButtonTd.appendChild(deleteButton);

      const tr = createElement("tr");
      tr.appendChild(nameTd);
      tr.appendChild(personTd);
      tr.appendChild(deadlineTd);
      tr.appendChild(deleteButtonTd);
      const tbody = getInputElementById("todo-list");
      tbody.appendChild(tr);
    });
};

完成アプリ

アプリURL

開発リポジトリ

感想

TypeScriptの基本と、関数型プログラミングについて学習しました。

Web制作でも、役割ごとに即時関数に切り出す。などはやっていましたが、もっと小さな粒度で切り出す方法を学びました。

命名規則だったり、分割の粒度のルールがあいまいなので、この辺を数こなして身につけたいです。

Web制作だと、一番混沌としやすい部分がCSSの設計だと思います。なので、Sassなどを用いてファイルを分割して役割を分担します。

おそらく、フロントエンドの場合JavaScriptが一番混沌としやすいところに置き換わるのかなと思いました。なので、関数型プログラミングで役割を細かく切り分けて、コードの可読性やデバックのしやすを向上する必要があると理解しています。

Yuta | Code.Yu

WordPressをメインに活動する、フリーランスのWeb制作コーダーです。
React案件を経験したことをきっかけに、さらにフロントエンド開発のスキルを高めるため、JavaScriptやReactの学習を進めています。このブログでは、学習の過程や記録を発信しています。

Web制作に関する情報はこちら
Code.Yu | ホームページ制作・コーディング代行 ↗︎