はじめに
JavaScriptは触ったことあるけどTypeScriptは軽くしか触ってない、という人向けに書く。
この記事では:
- TypeScriptの基礎(JSと違うところ中心)
- Honoのセットアップ
- Todo APIの実装
ここまでを一本で通す。
1. セットアップ
Node.jsがインストール済み前提で進める。
mkdir todo-hono
cd todo-hono
npm init -y
npm install typescript tsx --save-dev
npx tsc --init
tsconfig.json が生成される。今回はデフォルトのままでOK。
動作確認用に index.ts を作る。エディタ(VSCodeなど)で新規ファイルとして作成し、以下を書いて保存する。
const message: string = "Hello, TypeScript!";
console.log(message);
npx tsx index.ts
Hello, TypeScript!
tsx はTypeScriptをコンパイルなしで直接実行できるツール。開発中はこれで十分。
Windows(PowerShell)で echo "..." > index.ts のようにリダイレクトでファイルを作ると、既定のエンコーディングがUTF-16やShift-JISになり、esbuild(tsx が内部で使っている)が文字化けとして読めず Unexpected "�" のようなエラーになることがある。エディタで直接ファイルを作成するか、PowerShellで作る場合は Set-Content -Encoding utf8NoBOM を使ってUTF-8(BOMなし)を明示する。
2. TypeScript基礎
型注釈
let age: number = 30;
let name: string = "Alice";
let isActive: boolean = true;
// 型推論が効くので明示しなくてもいい場合が多い
let count = 0; // number と推論される
JavaScriptとの一番の違いは、変数・引数・戻り値に「型」を書けること。書かなくても動くコードもあるが、型があることでバグをコンパイル時に検出できる。
関数の型
function add(a: number, b: number): number {
return a + b;
}
// アロー関数でも同様
const multiply = (a: number, b: number): number => a * b;
引数の型を間違えると、実行前(コンパイル時)にエラーになる。
add("1", "2"); // Argument of type 'string' is not assignable to parameter of type 'number'
インターフェース
オブジェクトの形を定義する。
interface Todo {
id: number;
title: string;
done: boolean;
}
const todo: Todo = {
id: 1,
title: "買い物",
done: false,
};
必須プロパティが足りないとコンパイルエラーになる。? をつけるとオプショナル(任意)になる。
interface Todo {
id: number;
title: string;
done: boolean;
memo?: string; // あってもなくてもいい
}
型エイリアス
type でも似たようなことができる。
type Status = "pending" | "done" | "archived";
function updateStatus(status: Status) {
console.log(status);
}
updateStatus("pending"); // OK
updateStatus("invalid"); // エラー:Status型にない値
このような「決まった文字列のどれか」という型をUnion型(合併型)と呼ぶ。Todoの完了状態などに使うと、タイプミスをコンパイル時に防げる。
ジェネリクス
型を後から指定できる「型のテンプレート」。
function wrapInArray<T>(value: T): T[] {
return [value];
}
wrapInArray(1); // number[]
wrapInArray("hello"); // string[]
wrapInArray(true); // boolean[]
<T> の部分が「呼び出し時に決まる型」を表す。配列や後述のAPIレスポンスの型でよく使う。
3. Honoのセットアップ
Honoを追加する。
npm install hono
npm install @hono/node-server
src/index.ts を作る:
import { Hono } from "hono";
import { serve } from "@hono/node-server";
const app = new Hono();
app.get("/", (c) => c.text("Hello, Hono!"));
serve({
fetch: app.fetch,
port: 3000,
});
console.log("サーバー起動: http://localhost:3000");
実行:
npx tsx src/index.ts
curl http://localhost:3000
# Hello, Hono!
4. Todo APIを実装する
データの型を定義する
src/types.ts:
export interface Todo {
id: number;
title: string;
done: boolean;
}
export interface CreateTodoBody {
title: string;
}
インメモリストアを作る
src/store.ts:
import type { Todo } from "./types";
let todos: Todo[] = [];
let nextId = 1;
export const todoStore = {
getAll(): Todo[] {
return todos;
},
add(title: string): Todo {
const todo: Todo = { id: nextId++, title, done: false };
todos.push(todo);
return todo;
},
markDone(id: number): Todo | undefined {
const todo = todos.find((t) => t.id === id);
if (todo) {
todo.done = true;
}
return todo;
},
};
Todo | undefined という書き方もUnion型。「見つかればTodo、見つからなければundefined」という戻り値を型で表現している。
ルーティングを実装する
src/index.ts を以下で置き換える:
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { todoStore } from "./store";
import type { CreateTodoBody } from "./types";
const app = new Hono();
// 一覧取得
app.get("/todos", (c) => {
return c.json(todoStore.getAll());
});
// 追加
app.post("/todos", async (c) => {
const body = await c.req.json<CreateTodoBody>();
if (!body.title || body.title.trim() === "") {
return c.json({ error: "title is required" }, 400);
}
const todo = todoStore.add(body.title);
return c.json(todo, 201);
});
// 完了にする
app.put("/todos/:id/done", (c) => {
const id = Number(c.req.param("id"));
const todo = todoStore.markDone(id);
if (!todo) {
return c.json({ error: "todo not found" }, 404);
}
return c.json(todo);
});
serve({
fetch: app.fetch,
port: 3000,
});
console.log("サーバー起動: http://localhost:3000");
c.req.json<CreateTodoBody>() のようにジェネリクスで型を指定すると、リクエストボディに型がつく。基礎で説明したジェネリクスがここで実際に使われている。
5. 動作確認
npx tsx src/index.ts
Todo追加
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"title": "買い物"}'
{"id":1,"title":"買い物","done":false}
一覧取得
curl http://localhost:3000/todos
[{"id":1,"title":"買い物","done":false}]
完了にする
curl -X PUT http://localhost:3000/todos/1/done
{"id":1,"title":"買い物","done":true}
バリデーション確認
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"title": ""}'
{"error":"title is required"}
TypeScriptの型がAPIでどう役立つか
| 箇所 | 型の役割 |
|---|---|
Todo インターフェース |
レスポンスの形を保証する |
CreateTodoBody |
リクエストボディの形を明示する |
Todo | undefined |
「見つからない場合」をコンパイル時に意識させる |
c.req.json<CreateTodoBody>() |
パースしたJSONに型をつける |
JavaScriptだと body.titel(タイポ)のようなミスは実行時までわからないが、TypeScriptなら書いた瞬間にエディタが赤線で教えてくれる。特にAPIのリクエスト/レスポンスの形が変わりやすいプロジェクトほど、型の恩恵は大きい。
まとめ
| 要素 | 役割 |
|---|---|
interface |
オブジェクトの形を定義 |
Union型(|) |
複数の型のどれか、または「値かundefinedか」を表現 |
ジェネリクス(<T>) |
型を後から指定できるテンプレート |
| Hono | 軽量・型安全なWebフレームワーク |
Express経験者からすると、Honoは書き味が近いまま型の恩恵を強く受けられるのが特徴。c.req.json<T>() のようにフレームワーク側がジェネリクスを活用した設計になっているので、TypeScriptの型システムとの相性がいい。


