Day 29:総合実践 ― Todo管理アプリでTypeScriptを一気にまとめる
いよいよDay 29です。 ここまで学んできたTypeScriptの基本構文・Generics・Utility Types・Class・Moduleなどを、 ひとつの「Todo管理アプリ」にぎゅっと詰め込んで総復習していきます。
今回のゴールは、
「シンプルだけど、ちゃんとTypeScriptらしさが詰まっているTodo管理アプリ」を作ること
です。
Todo管理アプリの全体像をイメージする
Todo型の定義
まず、今回の主役となる Todo 型は次のように定義します。
type Todo = {
id: number;
title: string;
completed: boolean;
priority: "low" | "medium" | "high";
};
TypeScriptこの型をベースに、次の必須機能を実装していきます。
- Todo追加
- Todo削除
- Todo編集
- 完了・未完了切り替え
- 優先度変更
- 検索
- 絞り込み
- 並び替え
そして、使うTypeScriptの要素は次のとおりです。
- 基本型(
number/string/booleanなど) - 配列(
Todo[]) - Object(
Todo) - Function(関数型)
- Type Alias(
type Todo = ...) - Union Type(
"low" | "medium" | "high") - Narrowing(条件分岐で型を絞る)
- Generics(汎用関数)
- Utility Types(
Partial/Pickなど) - Module(ファイル分割)
ここからは、モジュール構成を決めて、 少しずつ組み立てていきます。
モジュール構成を決める
今回は、次の4ファイル構成で進めます。
types.ts- Todo型や関連する型定義をまとめる
todoRepository.ts- Todoの追加・削除・編集などのロジックをまとめる
todoUtils.ts- 検索・絞り込み・並び替えなどの汎用関数をまとめる
main.ts- 実際にTodo管理アプリを動かす「エントリーポイント」
この構成は、実務でもよく使われる「役割分担」のパターンです。
Step 1:types.ts ― Todo型と関連型を定義する
まずは、types.ts に型定義をまとめます。
// types.ts
// Todo管理アプリで使う型定義をまとめるファイル
// 優先度の型(Union Type)
export type Priority = "low" | "medium" | "high";
// Todoの型(Type Alias)
export type Todo = {
id: number; // 一意なID
title: string; // タイトル
completed: boolean; // 完了フラグ
priority: Priority; // 優先度
};
// Todoの更新用の型(Utility Typesを活用)
export type TodoUpdate = Partial<Pick<Todo, "title" | "completed" | "priority">>;
/*
TodoUpdateは、
- title
- completed
- priority
のいずれか(または複数)を「任意で」更新できる型です。
つまり、編集機能で「一部だけ変更したい」ときに使えます。
*/
TypeScriptここでの重要ポイント:
PriorityはUnion Typeで、「low / medium / high」の3つに絞っています。TodoUpdateはPartialとPickを組み合わせたUtility Typesの活用例です。Pick<Todo, "title" | "completed" | "priority">で、Todoから一部のプロパティだけを抜き出し、Partial<...>で「全部任意(オプション)」にしています。- これにより、「タイトルだけ変える」「優先度だけ変える」といった更新が自然に書けます。
Step 2:todoRepository.ts ― Todoの基本操作をまとめる
次に、Todoの追加・削除・編集などの基本操作を todoRepository.ts にまとめます。
// todoRepository.ts
// Todoの追加・削除・編集などを担当するモジュール
import { Todo, Priority, TodoUpdate } from "./types";
// 内部で管理するTodo一覧(配列)
let todos: Todo[] = [];
// 次のIDを生成するためのカウンタ
let nextId = 1;
// Todoを追加する関数
export function addTodo(title: string, priority: Priority = "medium"): Todo {
const newTodo: Todo = {
id: nextId++,
title,
completed: false,
priority,
};
todos.push(newTodo);
return newTodo;
}
// Todoを削除する関数
export function removeTodo(id: number): boolean {
const beforeLength = todos.length;
todos = todos.filter((todo) => todo.id !== id);
const afterLength = todos.length;
// 削除できたかどうかをbooleanで返す
return afterLength < beforeLength;
}
// Todoを編集する関数(Utility Typesを活用)
export function updateTodo(id: number, update: TodoUpdate): Todo | null {
const index = todos.findIndex((todo) => todo.id === id);
if (index === -1) {
return null; // 見つからなかった
}
const oldTodo = todos[index];
// スプレッド構文で部分更新
const newTodo: Todo = {
...oldTodo,
...update,
};
todos[index] = newTodo;
return newTodo;
}
// 完了・未完了を切り替える関数
export function toggleTodoCompleted(id: number): Todo | null {
const todo = todos.find((t) => t.id === id);
if (!todo) {
return null;
}
todo.completed = !todo.completed;
return todo;
}
// 優先度を変更する関数
export function changeTodoPriority(id: number, priority: Priority): Todo | null {
const todo = todos.find((t) => t.id === id);
if (!todo) {
return null;
}
todo.priority = priority;
return todo;
}
// Todo一覧を取得する関数
export function getTodos(): Todo[] {
// 外から直接いじられないようにコピーを返す
return [...todos];
}
TypeScriptここでの重要ポイント:
todosとnextIdはexportしていないので、 モジュール内部の「隠れた状態」として扱われます。updateTodoでTodoUpdateを使うことで、 「一部だけ更新する」という操作が型安全に書けています。toggleTodoCompletedやchangeTodoPriorityは、 「完了・未完了」「優先度変更」の必須機能を担当しています。
Step 3:todoUtils.ts ― 検索・絞り込み・並び替えを汎用関数で書く
次に、検索・絞り込み・並び替えを todoUtils.ts にまとめます。 ここでは、Genericsも少し使ってみます。
// todoUtils.ts
// 検索・絞り込み・並び替えなどの汎用関数をまとめるモジュール
import { Todo, Priority } from "./types";
// タイトルにキーワードを含むTodoを検索する関数
export function searchTodos(todos: Todo[], keyword: string): Todo[] {
const lower = keyword.toLowerCase();
return todos.filter((todo) => todo.title.toLowerCase().includes(lower));
}
// 完了・未完了で絞り込む関数
export function filterByCompleted(todos: Todo[], completed: boolean): Todo[] {
return todos.filter((todo) => todo.completed === completed);
}
// 優先度で絞り込む関数
export function filterByPriority(todos: Todo[], priority: Priority): Todo[] {
return todos.filter((todo) => todo.priority === priority);
}
// 並び替え:優先度順(high → medium → low)
export function sortByPriority(todos: Todo[]): Todo[] {
const order: Priority[] = ["high", "medium", "low"];
return [...todos].sort((a, b) => {
const aIndex = order.indexOf(a.priority);
const bIndex = order.indexOf(b.priority);
return aIndex - bIndex;
});
}
// 並び替え:タイトルのアルファベット順
export function sortByTitle(todos: Todo[]): Todo[] {
return [...todos].sort((a, b) => a.title.localeCompare(b.title));
}
// 汎用ソート関数(Genericsを活用)
export function sortByKey<T, K extends keyof T>(
items: T[],
key: K
): T[] {
return [...items].sort((a, b) => {
const aValue = a[key];
const bValue = b[key];
// Narrowing:stringの場合とnumberの場合で処理を分ける
if (typeof aValue === "string" && typeof bValue === "string") {
return aValue.localeCompare(bValue);
}
if (typeof aValue === "number" && typeof bValue === "number") {
return aValue - bValue;
}
// それ以外の型はそのまま(順序なし)
return 0;
});
}
TypeScriptここでの重要ポイント:
searchTodos/filterByCompleted/filterByPriority/sortByPriority/sortByTitleが、 Todo専用の検索・絞り込み・並び替え関数です。sortByKey<T, K extends keyof T>は、 Generics + keyof + Narrowing を組み合わせた汎用ソート関数です。K extends keyof Tによって、「Tのプロパティ名だけ」をキーとして受け取ります。typeof aValue === "string"などの条件分岐で、 型に応じて処理を変える「Narrowing」を行っています。
Step 4:main.ts ― Todo管理アプリを実際に動かす
最後に、main.ts で全体を組み立てて、 Todo管理アプリをコンソール上で動かしてみます。
// main.ts
// Todo管理アプリのエントリーポイント
import { Priority } from "./types";
import {
addTodo,
removeTodo,
updateTodo,
toggleTodoCompleted,
changeTodoPriority,
getTodos,
} from "./todoRepository";
import {
searchTodos,
filterByCompleted,
filterByPriority,
sortByPriority,
sortByTitle,
sortByKey,
} from "./todoUtils";
// Todo一覧を表示する関数
function printTodos(label: string, todos = getTodos()): void {
console.log(`\n=== ${label} ===`);
if (todos.length === 0) {
console.log("Todoはまだありません");
return;
}
todos.forEach((todo) => {
const status = todo.completed ? "✅ 完了" : "⏳ 未完了";
console.log(
`ID: ${todo.id} / ${status} / [${todo.priority}] ${todo.title}`
);
});
}
// 実行例
function main(): void {
console.log("=== Todo管理アプリ(総合実践) ===");
// Todo追加
const t1 = addTodo("TypeScriptの勉強をする", "high");
const t2 = addTodo("買い物に行く", "medium");
const t3 = addTodo("部屋を片付ける", "low");
const t4 = addTodo("ランニングをする", "medium");
printTodos("初期状態");
// 完了・未完了切り替え
toggleTodoCompleted(t2.id); // 買い物を完了にする
toggleTodoCompleted(t4.id); // ランニングを完了にする
printTodos("完了・未完了を更新後");
// 優先度変更
changeTodoPriority(t3.id, "high"); // 部屋片付けを高優先度に
printTodos("優先度変更後");
// Todo編集(タイトル変更)
updateTodo(t1.id, { title: "TypeScriptのGenericsを復習する" });
printTodos("タイトル編集後");
// 検索
const searchResult = searchTodos(getTodos(), "する");
printTodos("「する」を含むTodoの検索結果", searchResult);
// 絞り込み:完了のみ
const completedTodos = filterByCompleted(getTodos(), true);
printTodos("完了したTodoのみ", completedTodos);
// 絞り込み:優先度highのみ
const highPriorityTodos = filterByPriority(getTodos(), "high");
printTodos("優先度highのみ", highPriorityTodos);
// 並び替え:優先度順
const sortedByPriority = sortByPriority(getTodos());
printTodos("優先度順に並び替え", sortedByPriority);
// 並び替え:タイトル順
const sortedByTitle = sortByTitle(getTodos());
printTodos("タイトル順に並び替え", sortedByTitle);
// 汎用ソート関数でID順に並び替え
const sortedById = sortByKey(getTodos(), "id");
printTodos("汎用ソート関数でID順に並び替え", sortedById);
// Todo削除
removeTodo(t2.id); // 買い物のTodoを削除
printTodos("削除後の一覧");
}
// エントリーポイントを呼び出す
main();
TypeScriptここでの重要ポイント:
printTodos関数で、Todo一覧の表示ロジックをひとまとめにしています。main関数の中で、- 追加
- 完了・未完了切り替え
- 優先度変更
- 編集
- 検索
- 絞り込み
- 並び替え
- 削除 を順番に実行しながら、状態の変化をコンソールに出しています。
sortByKey(getTodos(), "id")のように、 Genericsを使った汎用ソート関数を実際に使っているのもポイントです。
Day 29用テンプレート一式(まとめて確認用)
最後に、4ファイル分をまとめて確認できるように、 もう一度コードを一覧しておきます。
types.ts
// types.ts
export type Priority = "low" | "medium" | "high";
export type Todo = {
id: number;
title: string;
completed: boolean;
priority: Priority;
};
export type TodoUpdate = Partial<Pick<Todo, "title" | "completed" | "priority">>;
TypeScripttodoRepository.ts
// todoRepository.ts
import { Todo, Priority, TodoUpdate } from "./types";
let todos: Todo[] = [];
let nextId = 1;
export function addTodo(title: string, priority: Priority = "medium"): Todo {
const newTodo: Todo = {
id: nextId++,
title,
completed: false,
priority,
};
todos.push(newTodo);
return newTodo;
}
export function removeTodo(id: number): boolean {
const beforeLength = todos.length;
todos = todos.filter((todo) => todo.id !== id);
const afterLength = todos.length;
return afterLength < beforeLength;
}
export function updateTodo(id: number, update: TodoUpdate): Todo | null {
const index = todos.findIndex((todo) => todo.id === id);
if (index === -1) {
return null;
}
const oldTodo = todos[index];
const newTodo: Todo = {
...oldTodo,
...update,
};
todos[index] = newTodo;
return newTodo;
}
export function toggleTodoCompleted(id: number): Todo | null {
const todo = todos.find((t) => t.id === id);
if (!todo) {
return null;
}
todo.completed = !todo.completed;
return todo;
}
export function changeTodoPriority(id: number, priority: Priority): Todo | null {
const todo = todos.find((t) => t.id === id);
if (!todo) {
return null;
}
todo.priority = priority;
return todo;
}
export function getTodos(): Todo[] {
return [...todos];
}
TypeScripttodoUtils.ts
// todoUtils.ts
import { Todo, Priority } from "./types";
export function searchTodos(todos: Todo[], keyword: string): Todo[] {
const lower = keyword.toLowerCase();
return todos.filter((todo) => todo.title.toLowerCase().includes(lower));
}
export function filterByCompleted(todos: Todo[], completed: boolean): Todo[] {
return todos.filter((todo) => todo.completed === completed);
}
export function filterByPriority(todos: Todo[], priority: Priority): Todo[] {
return todos.filter((todo) => todo.priority === priority);
}
export function sortByPriority(todos: Todo[]): Todo[] {
const order: Priority[] = ["high", "medium", "low"];
return [...todos].sort((a, b) => {
const aIndex = order.indexOf(a.priority);
const bIndex = order.indexOf(b.priority);
return aIndex - bIndex;
});
}
export function sortByTitle(todos: Todo[]): Todo[] {
return [...todos].sort((a, b) => a.title.localeCompare(b.title));
}
export function sortByKey<T, K extends keyof T>(items: T[], key: K): T[] {
return [...items].sort((a, b) => {
const aValue = a[key];
const bValue = b[key];
if (typeof aValue === "string" && typeof bValue === "string") {
return aValue.localeCompare(bValue);
}
if (typeof aValue === "number" && typeof bValue === "number") {
return aValue - bValue;
}
return 0;
});
}
TypeScriptmain.ts
// main.ts
import { Priority } from "./types";
import {
addTodo,
removeTodo,
updateTodo,
toggleTodoCompleted,
changeTodoPriority,
getTodos,
} from "./todoRepository";
import {
searchTodos,
filterByCompleted,
filterByPriority,
sortByPriority,
sortByTitle,
sortByKey,
} from "./todoUtils";
function printTodos(label: string, todos = getTodos()): void {
console.log(`\n=== ${label} ===`);
if (todos.length === 0) {
console.log("Todoはまだありません");
return;
}
todos.forEach((todo) => {
const status = todo.completed ? "✅ 完了" : "⏳ 未完了";
console.log(
`ID: ${todo.id} / ${status} / [${todo.priority}] ${todo.title}`
);
});
}
function main(): void {
console.log("=== Todo管理アプリ(総合実践) ===");
const t1 = addTodo("TypeScriptの勉強をする", "high");
const t2 = addTodo("買い物に行く", "medium");
const t3 = addTodo("部屋を片付ける", "low");
const t4 = addTodo("ランニングをする", "medium");
printTodos("初期状態");
toggleTodoCompleted(t2.id);
toggleTodoCompleted(t4.id);
printTodos("完了・未完了を更新後");
changeTodoPriority(t3.id, "high");
printTodos("優先度変更後");
updateTodo(t1.id, { title: "TypeScriptのGenericsを復習する" });
printTodos("タイトル編集後");
const searchResult = searchTodos(getTodos(), "する");
printTodos("「する」を含むTodoの検索結果", searchResult);
const completedTodos = filterByCompleted(getTodos(), true);
printTodos("完了したTodoのみ", completedTodos);
const highPriorityTodos = filterByPriority(getTodos(), "high");
printTodos("優先度highのみ", highPriorityTodos);
const sortedByPriority = sortByPriority(getTodos());
printTodos("優先度順に並び替え", sortedByPriority);
const sortedByTitle = sortByTitle(getTodos());
printTodos("タイトル順に並び替え", sortedByTitle);
const sortedById = sortByKey(getTodos(), "id");
printTodos("汎用ソート関数でID順に並び替え", sortedById);
removeTodo(t2.id);
printTodos("削除後の一覧");
}
main();
TypeScriptDay 29のまとめ
Day 29では、
- Todo型を中心にしたアプリ設計
- Type Alias / Union Type / Utility Types を使った型設計
- 配列・Object・Functionを組み合わせたロジック構成
- GenericsとNarrowingを使った汎用ソート関数
- Moduleによるファイル分割と役割分担
を、ひとつの「Todo管理アプリ」にまとめて体験しました。
ここまで来ると、 TypeScriptで「小さなアプリ」を自分の手で組み立てる感覚が、 かなりはっきりしてきているはずです。
このテンプレートをベースに、
- 期限(dueDate)を追加して、期限順の並び替えを実装する
completedとpriorityを組み合わせた複合絞り込みを作る- Moduleを増やして、UI層や永続化層(ローカルストレージなど)を分けてみる
など、少しずつ発展させていくことで、 TypeScriptの世界がさらに立体的に見えてくると思います。
