Lecture 1TypeScriptとは何か — JavaScriptに型の力を加える

12:00

TypeScriptとは何か — JavaScriptに型の力を加える

JavaScriptの悩み

JavaScriptは世界で最も使われているプログラミング言語です。Stack Overflow Developer Survey 2024で11年連続1位を記録しています。しかし、大規模開発ではある問題が顕著になります。

function calculateTotal(price, quantity) {
  return price * quantity;
}

// 意図しない使い方 — エラーにならない
console.log(calculateTotal("100", 3)); // "100100100"(文字列の繰り返し)
console.log(calculateTotal(100));       // NaN(quantityがundefined)

このコードは実行時まで間違いに気づけません。10万行のコードベースで、こうした暗黙のバグを目視で見つけるのは不可能です。

TypeScriptが解決すること

TypeScript はMicrosoftが2012年に公開したプログラミング言語で、JavaScriptに 静的型付け を追加します。開発者のアンダース・ヘルスバーグは、C#とDelphiの設計者でもあります。

function calculateTotal(price: number, quantity: number): number {
  return price * quantity;
}

calculateTotal("100", 3);  // コンパイルエラー! string は number に割り当てられない
calculateTotal(100);        // コンパイルエラー! 引数が足りない
calculateTotal(100, 3);     // OK: 300

型エラーはコードを書いた瞬間に検出されます。 実行する前に、エディタ上で赤い波線として表示されるのです。

TypeScriptの仕組み

TypeScriptは直接ブラウザやNode.jsで実行されません。トランスパイル(コンパイル)してJavaScriptに変換されます。

TypeScript (.ts)
    ↓ tsc(TypeScriptコンパイラ)
JavaScript (.js)
    ↓
ブラウザ / Node.js で実行

つまり、TypeScriptは「型チェック付きのJavaScript」です。型情報はコンパイル時にすべて消え、出力されるJavaScriptには型は一切含まれません。

環境構築

Node.jsのインストール

TypeScriptの実行にはNode.jsが必要です。https://nodejs.org から LTS版をインストールしてください。

# バージョン確認
node --version   # v20.x.x 以上を推奨
npm --version    # 10.x.x 以上

# TypeScriptのインストール
npm install -g typescript
tsc --version    # Version 5.x.x

最初のTypeScriptファイル

// hello.ts
const message: string = "Hello, TypeScript!";
console.log(message);
# コンパイルと実行
tsc hello.ts      # hello.js が生成される
node hello.js     # Hello, TypeScript!

tsconfig.json — プロジェクト設定

tsc --init  # tsconfig.json を自動生成
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

"strict": true は最も重要な設定です。これを有効にしないと、TypeScriptの型チェックの多くが無効化され、使う意味が半減します。

TypeScriptの採用状況

State of JS 2023 調査で、TypeScriptの満足度は約90%です。主要フレームワーク(Next.js, Angular, Svelte)はすべてTypeScriptをファーストクラスでサポートしています。

プロジェクト TypeScript採用
VS Code 全コードがTypeScript
Angular v2からTypeScript必須
Next.js デフォルトでTypeScript対応
Deno TypeScriptをネイティブ実行
Supabase クライアントSDKがTypeScript

GitHub Octoverse 2024で、TypeScriptはGitHub上の使用言語ランキング3位です。もはや「選択肢」ではなく「標準」になりつつあります。

TypeScript vs JavaScript — 何が変わるのか

// JavaScript → TypeScript の変化

// 1. 変数に型がつく
let count: number = 0;
let name: string = "太郎";
let isActive: boolean = true;

// 2. 関数の引数と戻り値に型がつく
function greet(name: string): string {
  return `こんにちは、${name}さん`;
}

// 3. オブジェクトの形が定義できる
interface User {
  id: number;
  name: string;
  email: string;
}

const user: User = {
  id: 1,
  name: "太郎",
  email: "taro@example.com",
};

TypeScriptを書くことは「仕様書を書きながらコードを書く」ようなものです。型定義がそのままドキュメントになるため、他の開発者がコードを読む際の理解速度が格段に上がります。

演習:最初のTypeScriptプログラム

// exercise.ts
// 課題:
// 1. 以下の関数に型注釈を追加してください
// 2. tsc exercise.ts でコンパイルし、エラーがないことを確認
// 3. 意図的に型エラーを起こし、エラーメッセージを確認

function add(a, b) {
  return a + b;
}

function formatUser(name, age) {
  return `${name} (${age}歳)`;
}

console.log(add(10, 20));
console.log(formatUser("太郎", 25));

次回は、TypeScriptの核心 — 基本的な型 を詳しく学びます。

参考文献

  • TypeScript公式ハンドブック: https://www.typescriptlang.org/docs/handbook/
  • TypeScript Deep Dive: https://basarat.gitbook.io/typescript/
  • Stack Overflow Developer Survey 2024: https://survey.stackoverflow.co/2024/

Lecture 2基本的な型 — TypeScriptの型システムを理解する

13:00

基本的な型 — TypeScriptの型システムを理解する

プリミティブ型

TypeScriptの基本型はJavaScriptのプリミティブに対応します。

// 数値
let price: number = 1980;
let pi: number = 3.14159;
let hex: number = 0xff;

// 文字列
let greeting: string = "こんにちは";
let template: string = `価格は${price}円です`;

// 真偽値
let isPublished: boolean = true;

// null と undefined
let nothing: null = null;
let notDefined: undefined = undefined;

型推論 — 型を書かなくても推論してくれる

TypeScriptは初期値から型を 推論 します。明らかな場合は型注釈を省略できます。

let count = 0;          // number と推論
let name = "太郎";      // string と推論
let active = true;      // boolean と推論

count = "hello";  // エラー! string を number に代入できない

ルール: 型が明らかな変数(初期化時に値を代入する場合)では型注釈を省略し、関数の引数と戻り値には明示的に型を書く。これが実務での標準的なスタイルです。

配列

// 配列の型定義(2つの書き方)
let numbers: number[] = [1, 2, 3];
let names: Array<string> = ["太郎", "花子"];

// 型安全な配列操作
numbers.push(4);       // OK
numbers.push("five");  // エラー! string は number[] に追加できない

// 読み取り専用配列
const colors: readonly string[] = ["red", "green", "blue"];
colors.push("yellow");  // エラー! readonly配列は変更不可

タプル

固定長で各位置の型が決まった配列です。

// [名前, 年齢, アクティブ]
let user: [string, number, boolean] = ["太郎", 25, true];

user[0].toUpperCase();  // OK — stringのメソッドが使える
user[1].toFixed(2);     // OK — numberのメソッドが使える
user[3];                // エラー! インデックス3は存在しない

タプルはAPIレスポンスのパース結果や、React の useState の戻り値([state, setState])のような「位置に意味がある短い配列」で使われます。

オブジェクト型

// インラインのオブジェクト型
let product: { name: string; price: number; inStock: boolean } = {
  name: "TypeScript入門書",
  price: 2980,
  inStock: true,
};

// オプショナルプロパティ(?をつける)
let config: { host: string; port?: number } = {
  host: "localhost",
  // port は省略可能
};

Union型 — 「AまたはB」

複数の型のいずれかを受け入れる型です。

// string または number
let id: string | number;
id = "abc-123";  // OK
id = 42;         // OK
id = true;       // エラー! boolean は不可

// 関数の引数で活用
function formatId(id: string | number): string {
  if (typeof id === "string") {
    return id.toUpperCase();  // string のメソッドが使える
  }
  return id.toFixed(0);      // number のメソッドが使える
}

typeof によるチェックを 型の絞り込み(Narrowing) と呼びます。TypeScriptはif文の条件から型を自動的に推論し、ブロック内で適切な型メソッドを提供します。

リテラル型

特定の値だけを受け入れる型です。

// "left" | "center" | "right" のいずれかのみ
let align: "left" | "center" | "right" = "center";
align = "top";  // エラー! "top" は許可されていない

// HTTP メソッド
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

function request(url: string, method: HttpMethod): void {
  console.log(`${method} ${url}`);
}

request("/api/users", "GET");     // OK
request("/api/users", "PATCH");   // エラー!

リテラル型は「マジックストリング」を型レベルで制約する強力な手法です。

any, unknown, never

// any — 型チェックを完全に無効化(使用は最小限に)
let dangerous: any = "hello";
dangerous.nonExistentMethod();  // エラーにならない!危険!

// unknown — 安全な any。使う前に型チェックが必要
let safe: unknown = "hello";
safe.toUpperCase();  // エラー! unknown のまま使えない

if (typeof safe === "string") {
  safe.toUpperCase();  // OK — string に絞り込んだ後
}

// never — 決して起こらない型
function throwError(message: string): never {
  throw new Error(message);
  // この関数は正常にreturnしない
}

any は TypeScript の非常口です。 JavaScript からの移行時には便利ですが、多用すると型チェックの恩恵が失われます。unknown で代替できないか常に検討してください。

型エイリアス

型に名前をつけて再利用できます。

type UserId = string | number;
type Status = "active" | "inactive" | "suspended";

type Product = {
  id: UserId;
  name: string;
  price: number;
  status: Status;
};

const item: Product = {
  id: "prod-001",
  name: "TypeScript入門書",
  price: 2980,
  status: "active",
};

型アサーション

TypeScriptの推論を「上書き」する手法です。DOM操作で頻出します。

// TypeScript は getElementById の戻り値を HTMLElement | null と推論
const input = document.getElementById("email") as HTMLInputElement;
input.value = "test@example.com";  // HTMLInputElement のプロパティにアクセス可能

// 非nullアサーション(! 演算子)
const element = document.getElementById("app")!;
// null ではないと断言(nullの場合は実行時エラー)

型アサーションは「TypeScriptより自分の方が型を正確に知っている」場合にのみ使います。乱用は型安全性を損なうため注意してください。

演習:型を追加する

// 課題: 以下のJavaScriptコードに適切な型注釈を追加してください

function createUser(name, age, email) {
  return { name, age, email, createdAt: new Date() };
}

function getUserAge(user) {
  return user.age;
}

const users = [
  createUser("太郎", 25, "taro@example.com"),
  createUser("花子", 30, "hanako@example.com"),
];

const ages = users.map(getUserAge);
const totalAge = ages.reduce((sum, age) => sum + age, 0);
console.log(`平均年齢: ${totalAge / users.length}`);

次回は、オブジェクトの形を厳密に定義する interfaceとtype を学びます。

参考文献

  • TypeScript Handbook - Everyday Types: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html
  • TypeScript Playground: https://www.typescriptlang.org/play

Lecture 3interfaceとtype — オブジェクトの形を定義する

13:00

interfaceとtype — オブジェクトの形を定義する

なぜオブジェクトの「形」を定義するのか

APIから返ってくるJSONの形、コンポーネントが受け取るprops、データベースのレコード — これらはすべて「こういうプロパティを持つオブジェクト」です。この「形」を事前に定義しておくことで、プロパティ名のタイポや型の不一致をコンパイル時に検出できます。

interface — オブジェクトの設計図

interface User {
  id: number;
  name: string;
  email: string;
  age?: number;           // オプショナル(省略可能)
  readonly createdAt: Date; // 読み取り専用
}

const user: User = {
  id: 1,
  name: "太郎",
  email: "taro@example.com",
  createdAt: new Date(),
};

user.name = "花子";       // OK — 変更可能
user.createdAt = new Date(); // エラー! readonly は変更不可
user.phone;               // エラー! phone は User に存在しない

interfaceの拡張(extends)

既存のinterfaceを継承して新しいinterfaceを作れます。

interface BaseEntity {
  id: number;
  createdAt: Date;
  updatedAt: Date;
}

interface User extends BaseEntity {
  name: string;
  email: string;
}

interface AdminUser extends User {
  role: "admin" | "superadmin";
  permissions: string[];
}

const admin: AdminUser = {
  id: 1,
  createdAt: new Date(),
  updatedAt: new Date(),
  name: "管理者",
  email: "admin@example.com",
  role: "admin",
  permissions: ["users.read", "users.write"],
};

extends による継承は、共通のプロパティを BaseEntity にまとめ、エンティティごとに固有のプロパティを追加するパターンで頻出します。

type — 型エイリアスの本領

typeinterface より柔軟です。Union型、交差型、プリミティブのエイリアスなど、あらゆる型に名前をつけられます。

// Union型(interfaceでは不可能)
type Status = "draft" | "published" | "archived";
type Id = string | number;

// 交差型(&)— 複数の型を合成
type Timestamped = {
  createdAt: Date;
  updatedAt: Date;
};

type User = {
  id: number;
  name: string;
};

type UserWithTimestamp = User & Timestamped;
// { id: number; name: string; createdAt: Date; updatedAt: Date }

interface vs type — 使い分け

機能 interface type
オブジェクト型の定義 OK OK
拡張 extends &(交差型)
Union型 不可 OK
プリミティブのエイリアス 不可 OK
宣言のマージ OK 不可
Reactのprops 推奨 OK
// 宣言のマージ(interfaceのみ)
interface Config {
  host: string;
}
interface Config {
  port: number;
}
// Config = { host: string; port: number } に自動マージ

// type では不可能
type Config2 = { host: string };
type Config2 = { port: number }; // エラー! 重複した識別子

実務の指針: オブジェクトの形を定義する場合は interface、Union型やプリミティブのエイリアスには type を使うのが一般的です。チーム内で統一されていればどちらでも問題ありません。

インデックスシグネチャ

キー名が事前にわからないオブジェクトの型を定義します。

// 文字列キー → 数値値のマップ
interface ScoreMap {
  [subject: string]: number;
}

const scores: ScoreMap = {
  math: 90,
  english: 85,
  science: 92,
};

// 既知のキーとインデックスシグネチャの組み合わせ
interface ApiResponse {
  status: number;
  message: string;
  [key: string]: unknown;  // 追加プロパティを許容
}

関数型

関数のシグネチャ(引数と戻り値の型)を定義できます。

// type で関数型を定義
type Formatter = (value: number) => string;

const formatCurrency: Formatter = (value) => {
  return `¥${value.toLocaleString()}`;
};

console.log(formatCurrency(19800));  // "¥19,800"

// interface でも定義可能(コールシグネチャ)
interface Calculator {
  (a: number, b: number): number;
}

const add: Calculator = (a, b) => a + b;
const multiply: Calculator = (a, b) => a * b;

ネストしたオブジェクト

実際のAPIレスポンスはネストした構造を持ちます。

interface Address {
  postalCode: string;
  prefecture: string;
  city: string;
  line: string;
}

interface Company {
  name: string;
  address: Address;
  employees: number;
}

interface UserProfile {
  id: number;
  name: string;
  company: Company;
  tags: string[];
  settings: {
    theme: "light" | "dark";
    language: "ja" | "en";
    notifications: boolean;
  };
}

型をネストさせることで、user.company.address.prefecture のような深いアクセスでも補完と型チェックが効きます。

Discriminated Union — 判別可能なUnion型

TypeScript最強のパターンの一つです。共通の「タグ」プロパティでオブジェクトの種類を判別します。

interface SuccessResponse {
  status: "success";
  data: { id: number; name: string };
}

interface ErrorResponse {
  status: "error";
  message: string;
  code: number;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse): void {
  switch (response.status) {
    case "success":
      // TypeScriptは response が SuccessResponse だと推論
      console.log(response.data.name);
      break;
    case "error":
      // TypeScriptは response が ErrorResponse だと推論
      console.log(`Error ${response.code}: ${response.message}`);
      break;
  }
}

status プロパティのリテラル型によって、switch文の各caseで型が自動的に絞り込まれます。APIクライアント、状態管理、イベントハンドリングなど、あらゆる場面で活躍します。

演習:ECサイトの型定義

// 課題: 以下のECサイトのデータ構造を型定義してください
// 1. Product interface(id, name, price, category, inStock)
// 2. CartItem(Product + quantity)
// 3. Order(id, items: CartItem[], total, status)
//    status は "pending" | "paid" | "shipped" | "delivered"
// 4. calculateTotal 関数の型を定義して実装

// ヒント:
// interface CartItem extends Product { ... }
// または type CartItem = Product & { quantity: number }

次回は、再利用可能な型を作る ジェネリクス を学びます。

参考文献

  • TypeScript Handbook - Object Types: https://www.typescriptlang.org/docs/handbook/2/objects.html
  • TypeScript Handbook - Narrowing: https://www.typescriptlang.org/docs/handbook/2/narrowing.html

Lecture 4ジェネリクス — 再利用可能な型を作る

14:00

ジェネリクス — 再利用可能な型を作る

同じロジック、違う型

配列の最初の要素を返す関数を考えます。

function getFirstNumber(arr: number[]): number | undefined {
  return arr[0];
}

function getFirstString(arr: string[]): string | undefined {
  return arr[0];
}

// ロジックは全く同じなのに、型ごとに関数を書くのは無駄

「型をパラメータ化する」仕組みが ジェネリクス(Generics) です。C#やJavaでも使われている概念で、TypeScriptの型システムの中核機能です。

ジェネリック関数

function getFirst<T>(arr: T[]): T | undefined {
  return arr[0];
}

// T は呼び出し時に具体的な型に置き換わる
const num = getFirst([1, 2, 3]);           // T = number → number | undefined
const str = getFirst(["a", "b", "c"]);     // T = string → string | undefined
const user = getFirst([{ name: "太郎" }]); // T = { name: string } → { name: string } | undefined

<T> は「型パラメータ」です。慣例で T(Type)、UVK(Key)、V(Value)が使われます。

ジェネリクスの実践的な使い方

APIレスポンスのラッパー

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

// 型パラメータ T に具体的な型を渡す
type UserResponse = ApiResponse<{ id: number; name: string }>;
type ProductListResponse = ApiResponse<{ id: number; name: string; price: number }[]>;

function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
  return fetch(url).then((res) => res.json());
}

// 呼び出し時に期待する型を指定
const users = await fetchApi<{ id: number; name: string }[]>("/api/users");
// users.data は { id: number; name: string }[] として型チェックされる

複数の型パラメータ

function mapObject<K extends string, V, R>(
  obj: Record<K, V>,
  fn: (value: V) => R
): Record<K, R> {
  const result = {} as Record<K, R>;
  for (const key in obj) {
    result[key] = fn(obj[key]);
  }
  return result;
}

const prices = { apple: 100, banana: 200, cherry: 300 };
const formatted = mapObject(prices, (v) => `¥${v}`);
// formatted: { apple: string; banana: string; cherry: string }

型制約(extends)

ジェネリクスの型パラメータに「最低限の条件」を付けます。

// T は { length: number } を持つ型でなければならない
function logLength<T extends { length: number }>(value: T): void {
  console.log(`Length: ${value.length}`);
}

logLength("hello");     // OK — string は length を持つ
logLength([1, 2, 3]);   // OK — 配列は length を持つ
logLength(42);          // エラー! number には length がない

keyof との組み合わせ

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: "太郎", age: 25, email: "taro@example.com" };

getProperty(user, "name");   // 戻り値: string
getProperty(user, "age");    // 戻り値: number
getProperty(user, "phone");  // エラー! "phone" は keyof User にない

keyof T は「Tのプロパティ名の Union型」を返します。これにより、存在しないプロパティ名を渡すとコンパイルエラーになります。

ジェネリッククラス

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get size(): number {
    return this.items.length;
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push("three");  // エラー! string は number に割り当てられない

const stringStack = new Stack<string>();
stringStack.push("hello");

ジェネリックインターフェース

// ページネーション付きレスポンス
interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  perPage: number;
  hasNext: boolean;
}

interface User {
  id: number;
  name: string;
}

interface Product {
  id: number;
  name: string;
  price: number;
}

// 同じ構造で異なるデータ型を格納
type UserPage = PaginatedResponse<User>;
type ProductPage = PaginatedResponse<Product>;

デフォルト型パラメータ

型パラメータにデフォルト値を設定できます。

interface EventPayload<T = Record<string, unknown>> {
  type: string;
  timestamp: Date;
  data: T;
}

// T を省略するとデフォルトが適用
const event1: EventPayload = {
  type: "pageview",
  timestamp: new Date(),
  data: { url: "/home" },
};

// T を明示的に指定
const event2: EventPayload<{ userId: number; action: string }> = {
  type: "click",
  timestamp: new Date(),
  data: { userId: 1, action: "purchase" },
};

よくあるジェネリクスのパターン

// 1. 配列操作
function filterByType<T>(arr: unknown[], guard: (x: unknown) => x is T): T[] {
  return arr.filter(guard);
}

// 2. Promise のラップ
async function withRetry<T>(fn: () => Promise<T>, retries: number): Promise<T> {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn();
    } catch (e) {
      if (i === retries - 1) throw e;
    }
  }
  throw new Error("Unreachable");
}

// 3. イベントエミッター
interface EventMap {
  login: { userId: number };
  logout: { userId: number };
  error: { message: string; code: number };
}

function emit<K extends keyof EventMap>(event: K, payload: EventMap[K]): void {
  console.log(`Event: ${event}`, payload);
}

emit("login", { userId: 1 });           // OK
emit("error", { message: "fail", code: 500 }); // OK
emit("login", { message: "wrong" });     // エラー! 型が合わない

演習:ジェネリックなユーティリティを作る

// 課題:
// 1. ジェネリックな groupBy 関数を実装
//    groupBy<T>(arr: T[], key: keyof T): Record<string, T[]>
//    例: groupBy(users, "city") → { "東京": [...], "大阪": [...] }

// 2. ジェネリックな Result 型を定義
//    type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E }
//    safeDivide(a: number, b: number): Result<number, string> を実装

// 3. ジェネリックな Cache クラスを実装
//    get(key: string): T | undefined
//    set(key: string, value: T, ttlMs: number): void
//    期限切れのエントリは get 時に自動削除

次回は、TypeScriptの型を変換・操作する ユーティリティ型 を学びます。

参考文献

  • TypeScript Handbook - Generics: https://www.typescriptlang.org/docs/handbook/2/generics.html
  • TypeScript Handbook - Keyof: https://www.typescriptlang.org/docs/handbook/2/keyof-types.html

Lecture 5ユーティリティ型と型操作 — 既存の型を変換する

14:00

ユーティリティ型と型操作 — 既存の型を変換する

型を「加工」する

実際の開発では、1つのエンティティに対して複数の型のバリエーションが必要になります。User の全プロパティが必須の型、一部だけ更新する型、IDだけ除いた作成用の型 — これらを毎回手書きするのは冗長です。

TypeScriptの ユーティリティ型 は、既存の型を変換して新しい型を生成する組み込みツールです。

Partial — 全プロパティをオプショナルに

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// 全プロパティが省略可能になる
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number }

function updateUser(id: number, updates: Partial<User>): void {
  // 名前だけ更新、メールだけ更新 — どちらもOK
  console.log(`Updating user ${id}:`, updates);
}

updateUser(1, { name: "花子" });           // OK
updateUser(1, { email: "new@example.com" }); // OK
updateUser(1, {});                          // OK(空でもエラーにならない)

Required — 全プロパティを必須に

Partial の逆です。オプショナルプロパティを必須にします。

interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}

type RequiredConfig = Required<Config>;
// { host: string; port: number; debug: boolean }

// デフォルト値で埋める関数
function createConfig(partial: Config): RequiredConfig {
  return {
    host: partial.host ?? "localhost",
    port: partial.port ?? 3000,
    debug: partial.debug ?? false,
  };
}

Pick — 特定のプロパティだけ取り出す

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
}

// 公開可能な情報だけ抽出
type PublicUser = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }

// APIレスポンスでパスワードを含めない
function getPublicProfile(user: User): PublicUser {
  return { id: user.id, name: user.name, email: user.email };
}

Omit — 特定のプロパティを除外

Pick の逆です。

// 作成時にはIDとタイムスタンプは不要
type CreateUserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; password: string }

function createUser(input: CreateUserInput): User {
  return {
    ...input,
    id: Math.random(),
    createdAt: new Date(),
  };
}

実務パターン: Omit<Entity, "id" | "createdAt" | "updatedAt"> で「作成用の入力型」を定義するのは非常に一般的です。

Record — キーと値の型を指定したオブジェクト

// ステータスごとのラベル定義
type Status = "draft" | "published" | "archived";

const statusLabels: Record<Status, string> = {
  draft: "下書き",
  published: "公開中",
  archived: "アーカイブ済み",
};

// すべてのステータスに対応が必須 — 1つでも漏れるとエラー
const statusColors: Record<Status, string> = {
  draft: "#gray",
  published: "#green",
  // archived が抜けている → コンパイルエラー!
};

Record<K, V> は「Kの全てのキーに対してV型の値を持つオブジェクト」を保証します。Union型のキーと組み合わせると、網羅性チェックが働きます。

Readonly — 全プロパティを読み取り専用に

interface State {
  count: number;
  items: string[];
}

const state: Readonly<State> = {
  count: 0,
  items: ["a", "b"],
};

state.count = 1;       // エラー! readonly プロパティ
state.items.push("c"); // 注意: これはエラーにならない(浅いReadonly)

Readonly はシャロー(浅い)です。ネストしたオブジェクトや配列の中身までは保護しません。深い不変性が必要な場合は as const を使います。

as const — 完全なリテラル型推論

// as const なし
const routes = {
  home: "/",
  about: "/about",
  users: "/users",
};
// 型: { home: string; about: string; users: string }

// as const あり
const routes2 = {
  home: "/",
  about: "/about",
  users: "/users",
} as const;
// 型: { readonly home: "/"; readonly about: "/about"; readonly users: "/users" }

// 値からUnion型を抽出
type Route = (typeof routes2)[keyof typeof routes2];
// "/" | "/about" | "/users"

Exclude と Extract — Union型の操作

type AllStatus = "draft" | "published" | "archived" | "deleted";

// 特定の型を除外
type ActiveStatus = Exclude<AllStatus, "deleted">;
// "draft" | "published" | "archived"

// 特定の型だけ抽出
type VisibleStatus = Extract<AllStatus, "draft" | "published">;
// "draft" | "published"

NonNullable — null と undefined を除外

type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string

ReturnType と Parameters — 関数の型を抽出

function createUser(name: string, age: number) {
  return { id: Date.now(), name, age };
}

type UserReturnType = ReturnType<typeof createUser>;
// { id: number; name: string; age: number }

type UserParams = Parameters<typeof createUser>;
// [name: string, age: number]

サードパーティライブラリの関数から型を抽出する際に便利です。

組み合わせの実践例

interface Product {
  id: number;
  name: string;
  description: string;
  price: number;
  category: string;
  imageUrl: string;
  createdAt: Date;
  updatedAt: Date;
}

// 作成用(IDとタイムスタンプを除外)
type CreateProduct = Omit<Product, "id" | "createdAt" | "updatedAt">;

// 更新用(全フィールドがオプショナル、ただしIDは不要)
type UpdateProduct = Partial<Omit<Product, "id" | "createdAt" | "updatedAt">>;

// 一覧表示用(軽量)
type ProductSummary = Pick<Product, "id" | "name" | "price" | "imageUrl">;

// 検索フィルター
type ProductFilter = Partial<Pick<Product, "category" | "price">>;

1つの Product 型から、用途に応じた4つの派生型を生成しています。元の型を変更すれば、派生型も自動的に追従します。

演習:ユーティリティ型を使いこなす

// 課題:
// 1. 以下の BlogPost 型から、用途別の型を派生させてください
interface BlogPost {
  id: number;
  title: string;
  content: string;
  author: string;
  tags: string[];
  status: "draft" | "published";
  publishedAt: Date | null;
  createdAt: Date;
  updatedAt: Date;
}

// - CreateBlogPost: 作成用(id, createdAt, updatedAt を除外)
// - UpdateBlogPost: 更新用(id を除外、全てオプショナル)
// - BlogPostCard: 一覧表示用(id, title, author, status, publishedAt のみ)

// 2. Record を使って、status ごとの BlogPost[] を格納するマップ型を定義

// 3. ReturnType を使って、以下の関数の戻り値型を抽出
function fetchPosts(page: number, limit: number) {
  return { posts: [] as BlogPost[], total: 0, hasNext: false };
}

次回は、モジュールの設計と非同期処理の型付けを学びます。

参考文献

  • TypeScript Handbook - Utility Types: https://www.typescriptlang.org/docs/handbook/utility-types.html
  • TypeScript Handbook - Mapped Types: https://www.typescriptlang.org/docs/handbook/2/mapped-types.html

Lecture 6関数の型付け — コールバック・オーバーロード・型ガード

13:00

関数の型付け — コールバック・オーバーロード・型ガード

関数の型注釈の基本

TypeScriptでは関数の引数と戻り値に型を指定します。

// 関数宣言
function add(a: number, b: number): number {
  return a + b;
}

// アロー関数
const multiply = (a: number, b: number): number => a * b;

// 戻り値の型推論(明示しなくても推論される)
const divide = (a: number, b: number) => a / b;
// 戻り値は number と自動推論

ルール: 公開API(exportする関数)には戻り値の型を明示し、内部関数やコールバックでは推論に任せるのが実務的なバランスです。

オプショナル引数とデフォルト値

// オプショナル引数(?)
function greet(name: string, greeting?: string): string {
  return `${greeting ?? "こんにちは"}、${name}さん`;
}
greet("太郎");              // "こんにちは、太郎さん"
greet("太郎", "おはよう");  // "おはよう、太郎さん"

// デフォルト値(型注釈は不要 — 値から推論される)
function createUser(name: string, role = "user") {
  return { name, role };
}
// role は string 型と推論される

オプショナル引数は必ず必須引数の後に配置します。

レストパラメータ

function sum(...numbers: number[]): number {
  return numbers.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3);       // 6
sum(10, 20, 30, 40); // 100

コールバック関数の型付け

// コールバックの型を明示
function fetchData(url: string, onSuccess: (data: unknown) => void, onError: (error: Error) => void): void {
  fetch(url)
    .then((res) => res.json())
    .then(onSuccess)
    .catch(onError);
}

// 型エイリアスでコールバック型を定義
type EventHandler<T> = (event: T) => void;

interface ClickEvent {
  x: number;
  y: number;
  target: string;
}

function onClick(handler: EventHandler<ClickEvent>): void {
  // handler の引数は ClickEvent として型チェックされる
  handler({ x: 100, y: 200, target: "button" });
}

onClick((event) => {
  console.log(`Clicked at (${event.x}, ${event.y})`);
});

関数オーバーロード

同じ関数名で異なる引数パターンに対応する場合に使います。

// オーバーロードシグネチャ
function createElement(tag: "a"): HTMLAnchorElement;
function createElement(tag: "canvas"): HTMLCanvasElement;
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: string): HTMLElement;

// 実装(引数を最も広い型で受ける)
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

const anchor = createElement("a");     // HTMLAnchorElement
const canvas = createElement("canvas"); // HTMLCanvasElement
const div = createElement("div");       // HTMLDivElement
const span = createElement("span");     // HTMLElement

呼び出し側の引数の型に応じて、戻り値の型が変わります。DOM操作やパーサーの実装で頻出するパターンです。

型ガード — 実行時の型チェック

typeof ガード

function formatValue(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase();   // string のメソッドが使える
  }
  return value.toFixed(2);       // number のメソッドが使える
}

instanceof ガード

class ApiError extends Error {
  constructor(
    message: string,
    public statusCode: number
  ) {
    super(message);
  }
}

function handleError(error: Error | ApiError): void {
  if (error instanceof ApiError) {
    console.log(`API Error ${error.statusCode}: ${error.message}`);
  } else {
    console.log(`Error: ${error.message}`);
  }
}

カスタム型ガード(is 演算子)

最も強力な型の絞り込みです。is キーワードで「この関数がtrueを返したら、引数は特定の型である」と宣言します。

interface Fish {
  swim: () => void;
}

interface Bird {
  fly: () => void;
}

// カスタム型ガード
function isFish(animal: Fish | Bird): animal is Fish {
  return "swim" in animal;
}

function move(animal: Fish | Bird): void {
  if (isFish(animal)) {
    animal.swim();  // Fish として認識
  } else {
    animal.fly();   // Bird として認識
  }
}

in 演算子による絞り込み

interface Admin {
  role: "admin";
  permissions: string[];
}

interface Guest {
  role: "guest";
  expiresAt: Date;
}

function getAccess(user: Admin | Guest): string[] {
  if ("permissions" in user) {
    return user.permissions;  // Admin
  }
  return ["read"];            // Guest
}

非同期関数の型付け

// async関数の戻り値は自動的に Promise<T> になる
async function fetchUser(id: number): Promise<{ name: string; email: string }> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return response.json();
}

// Promise の型パラメータ
type AsyncFn<T> = () => Promise<T>;

async function withTimeout<T>(fn: AsyncFn<T>, ms: number): Promise<T> {
  const result = await Promise.race([
    fn(),
    new Promise<never>((_, reject) =>
      setTimeout(() => reject(new Error("Timeout")), ms)
    ),
  ]);
  return result;
}

this の型付け

interface Button {
  label: string;
  onClick(this: Button): void;
}

const button: Button = {
  label: "送信",
  onClick() {
    console.log(`${this.label} がクリックされました`);
  },
};

button.onClick();  // OK — this は Button

const handler = button.onClick;
handler();  // エラー! this の型が void で Button と互換性がない

this パラメータは実際の引数ではなく、TypeScript独自の構文です。コンパイル後のJavaScriptからは消えます。

演習:型安全なイベントシステム

// 課題: 型安全なイベントエミッターを実装
// 1. EventMap を定義(イベント名 → ペイロードの型)
// 2. on(event, handler) — イベントリスナーを登録
// 3. emit(event, payload) — イベントを発火
// 4. off(event, handler) — リスナーを解除

// ヒント:
interface EventMap {
  userLogin: { userId: number; timestamp: Date };
  pageView: { url: string; referrer: string | null };
  error: { message: string; stack?: string };
}

// emitter.on("userLogin", (payload) => { ... })
// payload は自動的に { userId: number; timestamp: Date } と推論されるべき

次回は、モジュールシステムとプロジェクト設計を学びます。

参考文献

  • TypeScript Handbook - More on Functions: https://www.typescriptlang.org/docs/handbook/2/functions.html
  • TypeScript Handbook - Type Guards: https://www.typescriptlang.org/docs/handbook/2/narrowing.html

Lecture 7モジュールとプロジェクト設計 — スケーラブルなコード構成

13:00

モジュールとプロジェクト設計 — スケーラブルなコード構成

モジュールシステム

TypeScriptは ES Modules(import/export)を標準のモジュールシステムとして使います。

// user.ts — 名前付きエクスポート
export interface User {
  id: number;
  name: string;
  email: string;
}

export function createUser(name: string, email: string): User {
  return { id: Date.now(), name, email };
}

export const MAX_USERS = 1000;
// app.ts — インポート
import { User, createUser, MAX_USERS } from "./user";

const user: User = createUser("太郎", "taro@example.com");

デフォルトエクスポート

1ファイルにつき1つだけ設定できるエクスポートです。

// logger.ts
export default class Logger {
  log(message: string): void {
    console.log(`[${new Date().toISOString()}] ${message}`);
  }
}
// app.ts — デフォルトインポートは任意の名前で受け取れる
import Logger from "./logger";
const logger = new Logger();

実務の指針: 名前付きエクスポートが推奨されます。リファクタリング時のリネームが自動で追従し、IDE の補完も正確に動作します。デフォルトエクスポートはインポート名が自由すぎて、一貫性が保てなくなるリスクがあります。

再エクスポート

モジュールの「公開窓口」を作るパターンです。

// models/user.ts
export interface User { ... }

// models/product.ts
export interface Product { ... }

// models/index.ts — バレルファイル
export { User } from "./user";
export { Product } from "./product";

// 利用側
import { User, Product } from "./models";  // index.ts から一括インポート

tsconfig.json の重要な設定

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "sourceMap": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}
設定 意味
strict 全ての厳格チェックを有効化(必須)
declaration .d.ts 型定義ファイルを生成
sourceMap デバッグ用ソースマップを生成
paths パスエイリアス(@/utilssrc/utils
isolatedModules ファイル単位のトランスパイルを保証(esbuild等で必要)

型定義ファイル(.d.ts)

JavaScriptライブラリの型情報を提供するファイルです。DefinitelyTyped プロジェクトが数千のライブラリの型定義を管理しています。

# lodashの型定義をインストール
npm install lodash
npm install -D @types/lodash

# 型定義が不要なライブラリ(TypeScript製)
npm install zod         # 型定義が同梱されている
npm install axios       # 型定義が同梱されている
// @types/lodash のおかげで型チェックが効く
import _ from "lodash";

const sorted = _.sortBy([{ name: "花子", age: 30 }, { name: "太郎", age: 25 }], "age");
// sorted は { name: string; age: number }[] と推論

プロジェクト構成のパターン

フラットな構成(小規模プロジェクト)

src/
├── index.ts
├── types.ts         共通の型定義
├── utils.ts         ユーティリティ関数
├── api.ts           API呼び出し
└── config.ts        設定値

レイヤードアーキテクチャ(中規模)

src/
├── types/            型定義
   ├── user.ts
   ├── product.ts
   └── index.ts
├── services/         ビジネスロジック
   ├── userService.ts
   └── productService.ts
├── repositories/     データアクセス
   ├── userRepo.ts
   └── productRepo.ts
├── utils/            汎用ユーティリティ
   ├── validation.ts
   └── formatting.ts
└── index.ts

機能ベース(大規模)

src/
├── features/
   ├── auth/
      ├── types.ts
      ├── authService.ts
      └── authMiddleware.ts
   ├── users/
      ├── types.ts
      ├── userService.ts
      └── userRouter.ts
   └── products/
       ├── types.ts
       ├── productService.ts
       └── productRouter.ts
├── shared/
   ├── types.ts
   └── utils.ts
└── index.ts

名前空間 vs モジュール

TypeScript 初期の namespace はレガシー機能です。現代のTypeScriptではES Modulesを使います。

// 非推奨(namespace)
namespace MyApp {
  export interface User { id: number }
}

// 推奨(ESモジュール)
// types/user.ts
export interface User { id: number }

環境変数の型定義

// env.d.ts — 環境変数に型をつける
declare namespace NodeJS {
  interface ProcessEnv {
    NODE_ENV: "development" | "production" | "test";
    DATABASE_URL: string;
    API_KEY: string;
    PORT?: string;
  }
}

// 使用時(型チェックと補完が効く)
const port = parseInt(process.env.PORT ?? "3000");
const dbUrl = process.env.DATABASE_URL;  // string(必須なので undefined にならない)

zodによるランタイムバリデーション

TypeScriptの型はコンパイル時にしか存在しません。外部からの入力(APIリクエスト、環境変数)は実行時にも検証が必要です。zodは型定義とバリデーションを統合するライブラリです。

import { z } from "zod";

// スキーマ定義(バリデーションルール + 型定義を兼ねる)
const UserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
});

// スキーマから TypeScript の型を自動生成
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age: number }

// バリデーション
const result = UserSchema.safeParse({
  name: "太郎",
  email: "taro@example.com",
  age: 25,
});

if (result.success) {
  const user: User = result.data;  // 型安全
} else {
  console.error(result.error.issues);
}

zodを使えば「型定義とバリデーションルールの二重管理」を避けられます。

演習:TypeScriptプロジェクトを構築する

# 課題:
# 1. 新しいプロジェクトを作成
mkdir ts-practice && cd ts-practice
npm init -y
npm install typescript @types/node -D
npx tsc --init

# 2. src/ ディレクトリにレイヤードアーキテクチャで以下を実装
#    - types/todo.ts: Todo interface
#    - services/todoService.ts: CRUD 関数(メモリ内配列で管理)
#    - index.ts: サービスを使ってTodoを操作

# 3. "strict": true でコンパイルエラーが0であることを確認
# 4. zod でTodo作成時のバリデーションを追加

次回は、TypeScriptによるエラーハンドリングと型安全なパターンを学びます。

参考文献

  • TypeScript Handbook - Modules: https://www.typescriptlang.org/docs/handbook/2/modules.html
  • TypeScript tsconfig Reference: https://www.typescriptlang.org/tsconfig
  • zod Documentation: https://zod.dev/

Lecture 8エラーハンドリングと型安全パターン — 堅牢なコードを書く

13:00

エラーハンドリングと型安全パターン — 堅牢なコードを書く

TypeScriptのエラーハンドリングの課題

JavaScriptの try/catch には型の問題があります。catch の引数は常に unknown 型です。

try {
  const data = JSON.parse(invalidJson);
} catch (error) {
  // error は unknown 型 — 何のプロパティにもアクセスできない
  console.log(error.message);  // エラー!

  // 型を絞り込む必要がある
  if (error instanceof Error) {
    console.log(error.message);  // OK
  }
}

Result パターン — 例外を使わないエラーハンドリング

Rust の Result<T, E> に着想を得たパターンです。関数の戻り値で成功と失敗を明示します。

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) {
    return { ok: false, error: "ゼロで割ることはできません" };
  }
  return { ok: true, value: a / b };
}

const result = divide(10, 0);
if (result.ok) {
  console.log(result.value);  // number — 型安全
} else {
  console.log(result.error);  // string — 型安全
}

Result パターンの利点は、エラーケースの処理を強制することです。try/catch では catch を書き忘れても警告が出ませんが、Result 型では ok のチェックなしに value にアクセスするとコンパイルエラーになります。

// 非同期版
async function fetchUser(id: number): Promise<Result<User, ApiError>> {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) {
      return { ok: false, error: { status: res.status, message: res.statusText } };
    }
    const data = await res.json();
    return { ok: true, value: data };
  } catch {
    return { ok: false, error: { status: 0, message: "Network error" } };
  }
}

interface ApiError {
  status: number;
  message: string;
}

カスタムエラークラス

エラーの種類を型で区別するパターンです。

class NotFoundError extends Error {
  constructor(
    public resource: string,
    public id: string | number
  ) {
    super(`${resource} with id ${id} not found`);
    this.name = "NotFoundError";
  }
}

class ValidationError extends Error {
  constructor(
    public field: string,
    public reason: string
  ) {
    super(`Validation failed for ${field}: ${reason}`);
    this.name = "ValidationError";
  }
}

class UnauthorizedError extends Error {
  constructor() {
    super("Authentication required");
    this.name = "UnauthorizedError";
  }
}

type AppError = NotFoundError | ValidationError | UnauthorizedError;

function handleError(error: AppError): { status: number; message: string } {
  if (error instanceof NotFoundError) {
    return { status: 404, message: error.message };
  }
  if (error instanceof ValidationError) {
    return { status: 400, message: `${error.field}: ${error.reason}` };
  }
  if (error instanceof UnauthorizedError) {
    return { status: 401, message: error.message };
  }
  const _exhaustive: never = error;  // 全ケースを網羅したことを保証
  return _exhaustive;
}

Exhaustive Check — 網羅性チェック

switch文で全てのケースを処理したことをコンパイル時に保証する手法です。

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
    default: {
      const _exhaustive: never = shape;
      return _exhaustive;
    }
  }
}

将来 Shape に新しいバリアントを追加した場合、switch文に対応するcaseがなければコンパイルエラーが発生します。「処理し忘れ」を型レベルで防げる非常に強力なパターンです。

Branded Type — プリミティブに意味を持たせる

// UserId と ProductId を型レベルで区別
type UserId = number & { readonly __brand: "UserId" };
type ProductId = number & { readonly __brand: "ProductId" };

function createUserId(id: number): UserId {
  return id as UserId;
}

function createProductId(id: number): ProductId {
  return id as ProductId;
}

function getUser(id: UserId): void {
  console.log(`Fetching user ${id}`);
}

const userId = createUserId(1);
const productId = createProductId(1);

getUser(userId);     // OK
getUser(productId);  // エラー! ProductId は UserId に代入できない
getUser(42);         // エラー! number は UserId に代入できない

実行時にはどちらも単なる number ですが、コンパイル時には異なる型として扱われます。IDの取り違えというよくあるバグを型レベルで防止できます。

型安全なイベントハンドラー

interface EventMap {
  click: { x: number; y: number };
  keypress: { key: string; code: string };
  submit: { formData: Record<string, string> };
}

class TypedEmitter<T extends Record<string, unknown>> {
  private handlers = new Map<keyof T, Set<(payload: any) => void>>();

  on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set());
    }
    this.handlers.get(event)!.add(handler);
  }

  emit<K extends keyof T>(event: K, payload: T[K]): void {
    this.handlers.get(event)?.forEach((handler) => handler(payload));
  }
}

const emitter = new TypedEmitter<EventMap>();

emitter.on("click", (payload) => {
  console.log(payload.x, payload.y);  // 型安全: x, y は number
});

emitter.emit("click", { x: 100, y: 200 });        // OK
emitter.emit("click", { key: "a", code: "KeyA" }); // エラー! 型が合わない

Builder パターン

メソッドチェーンで型安全にオブジェクトを構築するパターンです。

class QueryBuilder<T extends Record<string, unknown>> {
  private conditions: string[] = [];
  private ordering: string | null = null;
  private limitValue: number | null = null;

  where(condition: string): this {
    this.conditions.push(condition);
    return this;
  }

  orderBy(field: keyof T & string, direction: "asc" | "desc" = "asc"): this {
    this.ordering = `${field} ${direction}`;
    return this;
  }

  limit(n: number): this {
    this.limitValue = n;
    return this;
  }

  build(): string {
    let query = "SELECT * FROM table";
    if (this.conditions.length) {
      query += ` WHERE ${this.conditions.join(" AND ")}`;
    }
    if (this.ordering) query += ` ORDER BY ${this.ordering}`;
    if (this.limitValue) query += ` LIMIT ${this.limitValue}`;
    return query;
  }
}

interface UserRow {
  id: number;
  name: string;
  createdAt: Date;
}

const query = new QueryBuilder<UserRow>()
  .where("name LIKE '%太郎%'")
  .orderBy("createdAt", "desc")
  .limit(10)
  .build();

演習:型安全なAPIクライアント

// 課題: 型安全なAPIクライアントを実装
// 1. Result<T, E> 型を定義
// 2. ApiClient クラスを作成
//    - get<T>(path: string): Promise<Result<T, ApiError>>
//    - post<T>(path: string, body: unknown): Promise<Result<T, ApiError>>
// 3. 各エンドポイントの型を定義
//    GET  /users    → User[]
//    GET  /users/:id → User
//    POST /users    → User
// 4. Exhaustive check で全エラーケースをハンドリング

次回は、React/Node.jsでのTypeScript実践を学びます。

参考文献

  • TypeScript Handbook - Narrowing: https://www.typescriptlang.org/docs/handbook/2/narrowing.html
  • Effect-TS: https://effect.website/ (型安全なエラーハンドリングライブラリ)

Lecture 9ReactとNode.jsでのTypeScript — フルスタック実践

14:00

ReactとNode.jsでのTypeScript — フルスタック実践

ReactでのTypeScript

ReactはTypeScriptと最も相性の良いフロントエンドフレームワークです。Meta社のReactチーム自身がTypeScriptでの開発を推奨しており、Create React App, Vite, Next.js すべてがTypeScriptテンプレートを提供しています。

Reactプロジェクトのセットアップ

# Vite + React + TypeScript
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm run dev

コンポーネントの型付け

// props の型定義
interface ButtonProps {
  label: string;
  variant?: "primary" | "secondary" | "danger";
  disabled?: boolean;
  onClick: () => void;
}

// 関数コンポーネント
function Button({ label, variant = "primary", disabled = false, onClick }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant}`}
      disabled={disabled}
      onClick={onClick}
    >
      {label}
    </button>
  );
}

// 使用側 — propsの型チェックが効く
<Button label="送信" onClick={() => console.log("clicked")} />
<Button label="削除" variant="danger" onClick={handleDelete} />
<Button label={42} />  // エラー! number は string に割り当てられない

children を含むコンポーネント

interface CardProps {
  title: string;
  children: React.ReactNode;  // JSX要素、文字列、数値、null など
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

<Card title="プロフィール">
  <p>名前: 太郎</p>
  <p>年齢: 25</p>
</Card>

useState の型付け

import { useState } from "react";

// 型推論が効く場合(初期値から推論)
const [count, setCount] = useState(0);        // number
const [name, setName] = useState("太郎");     // string

// 型を明示する必要がある場合
const [user, setUser] = useState<User | null>(null);
// 初期値が null で、後から User 型の値がセットされる

interface User {
  id: number;
  name: string;
}

// setUser に型チェックが効く
setUser({ id: 1, name: "太郎" });  // OK
setUser({ id: 1 });                // エラー! name が必要

useEffect とイベントハンドラー

import { useState, useEffect } from "react";

function UserList() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function fetchUsers() {
      const res = await fetch("/api/users");
      const data: User[] = await res.json();
      setUsers(data);
      setLoading(false);
    }
    fetchUsers();
  }, []);

  // イベントハンドラーの型
  const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
    const query = e.target.value;  // string と推論
    console.log(query);
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // フォーム送信処理
  };

  if (loading) return <p>読み込み中...</p>;

  return (
    <div>
      <input type="text" onChange={handleSearch} />
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

カスタムフックの型付け

function useLocalStorage<T>(key: string, initialValue: T) {
  const [stored, setStored] = useState<T>(() => {
    const item = localStorage.getItem(key);
    return item ? JSON.parse(item) : initialValue;
  });

  const setValue = (value: T | ((prev: T) => T)) => {
    const valueToStore = value instanceof Function ? value(stored) : value;
    setStored(valueToStore);
    localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  return [stored, setValue] as const;
  // as const で [T, (value: ...) => void] タプルとして推論
}

// 使用例
const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light");
setTheme("dark");    // OK
setTheme("blue");    // エラー!

Node.js(Express)でのTypeScript

セットアップ

mkdir api-server && cd api-server
npm init -y
npm install express
npm install -D typescript @types/node @types/express ts-node
npx tsc --init

型付きルーターの実装

import express, { Request, Response } from "express";

const app = express();
app.use(express.json());

// リクエスト・レスポンスの型定義
interface CreateUserBody {
  name: string;
  email: string;
}

interface UserResponse {
  id: number;
  name: string;
  email: string;
  createdAt: string;
}

// メモリ内ストア
const users: UserResponse[] = [];
let nextId = 1;

// GET /users
app.get("/users", (_req: Request, res: Response<UserResponse[]>) => {
  res.json(users);
});

// POST /users(リクエストボディの型を指定)
app.post("/users", (req: Request<{}, {}, CreateUserBody>, res: Response<UserResponse>) => {
  const { name, email } = req.body;
  const user: UserResponse = {
    id: nextId++,
    name,
    email,
    createdAt: new Date().toISOString(),
  };
  users.push(user);
  res.status(201).json(user);
});

// パスパラメータの型指定
app.get("/users/:id", (req: Request<{ id: string }>, res: Response) => {
  const id = parseInt(req.params.id);
  const user = users.find((u) => u.id === id);
  if (!user) {
    return res.status(404).json({ message: "User not found" });
  }
  res.json(user);
});

app.listen(3000, () => {
  console.log("Server running on port 3000");
});

ミドルウェアの型付け

import { Request, Response, NextFunction } from "express";

// 認証ミドルウェア
interface AuthenticatedRequest extends Request {
  userId?: number;
}

function authMiddleware(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
  const token = req.headers.authorization?.replace("Bearer ", "");
  if (!token) {
    res.status(401).json({ message: "Token required" });
    return;
  }
  // トークン検証(簡略化)
  req.userId = 1;
  next();
}

app.get("/profile", authMiddleware, (req: AuthenticatedRequest, res: Response) => {
  const userId = req.userId!;
  res.json({ userId });
});

フロントエンドとバックエンドの型共有

TypeScriptの最大の利点は、フロントエンドとバックエンドで型定義を共有できることです。

// shared/types.ts — 共有型定義
export interface User {
  id: number;
  name: string;
  email: string;
}

export interface CreateUserInput {
  name: string;
  email: string;
}

export interface ApiResponse<T> {
  data: T;
  message: string;
}
// フロントエンド(React)
import { User, ApiResponse } from "../shared/types";

async function getUsers(): Promise<User[]> {
  const res = await fetch("/api/users");
  const json: ApiResponse<User[]> = await res.json();
  return json.data;
}
// バックエンド(Express)
import { User, CreateUserInput, ApiResponse } from "../shared/types";

app.post("/users", (req: Request<{}, {}, CreateUserInput>, res: Response<ApiResponse<User>>) => {
  // 同じ型定義を参照
});

APIの型が変わったとき、フロントエンドとバックエンドの両方でコンパイルエラーが発生するため、型の不一致を即座に発見できます。

演習:フルスタックTypeScriptアプリ

// 課題:
// 1. Express + TypeScript でTodo API を作成
//    - GET  /todos       → Todo[]
//    - POST /todos       → Todo
//    - PUT  /todos/:id   → Todo
//    - DELETE /todos/:id → { message: string }

// 2. React + TypeScript でフロントエンドを作成
//    - Todo一覧の表示
//    - 新規作成フォーム
//    - 完了/未完了の切り替え

// 3. shared/types.ts で型を共有

// ヒント: shared型を monorepo で管理するか、
// npm パッケージとして公開する方法がある

次回の最終講義では、TypeScriptの高度な型テクニックと実務のベストプラクティスを総まとめします。

参考文献

  • React TypeScript Cheatsheet: https://react-typescript-cheatsheet.netlify.app/
  • Express TypeScript: https://expressjs.com/en/guide/using-with-typescript.html

Lecture 10実務のベストプラクティスと総まとめ — TypeScriptを使いこなす

14:00

実務のベストプラクティスと総まとめ — TypeScriptを使いこなす

strict モードを妥協しない

tsconfig.json"strict": true は TypeScript の効果を最大化するための最重要設定です。これは以下の個別フラグをすべて有効にします:

フラグ 効果
strictNullChecks null/undefined の暗黙的な許容を禁止
noImplicitAny 暗黙的な any を禁止
strictFunctionTypes 関数の引数の型チェックを厳格化
strictBindCallApply bind/call/apply の型チェック
strictPropertyInitialization クラスプロパティの初期化を必須に
// strictNullChecks が OFF の場合(危険)
const element = document.getElementById("app");
element.innerHTML = "hello";  // エラーにならない! null の可能性を無視

// strictNullChecks が ON の場合(安全)
const element = document.getElementById("app");
element.innerHTML = "hello";  // エラー! element は null かもしれない

if (element) {
  element.innerHTML = "hello";  // OK — null チェック後
}

any を根絶する

any はTypeScriptの型システムを完全に無効化します。

// 悪い例
function processData(data: any) {
  return data.items.map((item: any) => item.name.toUpperCase());
  // 実行時エラーの温床 — コンパイラは何もチェックしない
}

// 良い例
interface DataResponse {
  items: { name: string; id: number }[];
}

function processData(data: DataResponse) {
  return data.items.map((item) => item.name.toUpperCase());
  // 全てのプロパティアクセスが型チェックされる
}

any の代替手段: - unknown — 安全な any。使う前に型チェックが必要 - ジェネリクス <T> — 型をパラメータ化 - Union型 string | number — 具体的な候補を列挙

ESLint の @typescript-eslint/no-explicit-any ルールで any の使用を禁止できます。

型推論を活かす

型注釈は「必要な場所」にだけ書きます。

// 冗長 — 右辺から推論できる
const name: string = "太郎";
const numbers: number[] = [1, 2, 3];
const user: { name: string; age: number } = { name: "太郎", age: 25 };

// 簡潔 — 推論に任せる
const name = "太郎";
const numbers = [1, 2, 3];
const user = { name: "太郎", age: 25 };

// 型注釈が必要な場面
// 1. 関数の引数(推論できない)
function greet(name: string): string { ... }

// 2. 初期値がnullで後から代入する場合
const [user, setUser] = useState<User | null>(null);

// 3. 空の配列やオブジェクト
const items: Item[] = [];

Mapped Types と Conditional Types

ユーティリティ型の裏側にある高度な型操作です。

// Mapped Type — オブジェクト型のプロパティを変換
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

// Conditional Type — 条件分岐する型
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;  // "yes"
type B = IsString<number>;  // "no"

// 実用例: 配列の要素型を抽出
type ElementOf<T> = T extends (infer E)[] ? E : never;

type X = ElementOf<string[]>;  // string
type Y = ElementOf<number[]>;  // number

Template Literal Types

文字列リテラル型をテンプレートで生成できます。

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiPath = "/users" | "/products" | "/orders";

// 全組み合わせの型を自動生成
type ApiRoute = `${HttpMethod} ${ApiPath}`;
// "GET /users" | "GET /products" | "GET /orders" | "POST /users" | ...

// CSSプロパティの自動生成
type Breakpoint = "sm" | "md" | "lg" | "xl";
type BreakpointClass = `hidden-${Breakpoint}`;
// "hidden-sm" | "hidden-md" | "hidden-lg" | "hidden-xl"

ESLintとPrettierの設定

npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin prettier
// eslint.config.js(Flat Config)
import tsParser from "@typescript-eslint/parser";
import tsPlugin from "@typescript-eslint/eslint-plugin";

export default [
  {
    files: ["**/*.ts", "**/*.tsx"],
    languageOptions: {
      parser: tsParser,
      parserOptions: { project: "./tsconfig.json" },
    },
    plugins: { "@typescript-eslint": tsPlugin },
    rules: {
      "@typescript-eslint/no-explicit-any": "error",
      "@typescript-eslint/no-unused-vars": "error",
      "@typescript-eslint/consistent-type-imports": "error",
    },
  },
];

consistent-type-importsimport type { User } を強制するルールです。型のみのインポートはランタイムに含まれないため、バンドルサイズに影響しません。

テストとTypeScript

// Vitest での型安全なテスト
import { describe, it, expect } from "vitest";
import { calculateTotal } from "./cart";

interface CartItem {
  name: string;
  price: number;
  quantity: number;
}

describe("calculateTotal", () => {
  it("カート内の合計金額を計算する", () => {
    const items: CartItem[] = [
      { name: "TypeScript入門書", price: 2980, quantity: 1 },
      { name: "ノートPC", price: 98000, quantity: 1 },
    ];
    expect(calculateTotal(items)).toBe(100980);
  });

  it("空のカートは0を返す", () => {
    expect(calculateTotal([])).toBe(0);
  });
});

段階的な移行戦略

既存のJavaScriptプロジェクトをTypeScriptに移行する手順です。

// tsconfig.json — 段階的に厳格化
{
  "compilerOptions": {
    "allowJs": true,           // Step 1: JSファイルを許容
    "checkJs": false,          // Step 2: JSの型チェックは無効
    "strict": false,           // Step 3: 最初は緩めに
    // "strict": true,         // Step 4: 最終的にtrue
  }
}

移行の順序: 1. tsconfig.json を追加し、allowJs: true で既存コードをそのまま動かす 2. 共有の型定義ファイル(types.ts)を作成 3. 新規ファイルはすべて .ts で作成 4. ファイルごとに .js.ts にリネームし、型を追加 5. strict: true に切り替え、残りのエラーを修正

このコースの学習ロードマップ

講義 習得した技術
第1講 TypeScriptの全体像、環境構築、トランスパイル
第2講 プリミティブ型、配列、Union型、型推論
第3講 interface、type、Discriminated Union
第4講 ジェネリクス、型制約、keyof
第5講 ユーティリティ型(Partial, Pick, Omit, Record)
第6講 関数の型付け、型ガード、オーバーロード
第7講 モジュール、tsconfig、zod
第8講 Result型、Exhaustive Check、Branded Type
第9講 React + Node.js でのフルスタック実践
第10講 Mapped/Conditional Types、ESLint、移行戦略

次のステップ

方向 学ぶべきこと
フロントエンド Next.js + TypeScript(本サイトの別コース)
バックエンド Hono / tRPC(型安全なAPI)
フルスタック T3 Stack(Next.js + tRPC + Prisma + NextAuth)
ライブラリ開発 npmパッケージの公開、.d.ts の作り方
型パズル Type Challenges (https://github.com/type-challenges/type-challenges)

TypeScriptは「JavaScriptを書くすべての人」のためのツールです。型システムを理解すればするほど、バグの少ない、読みやすい、メンテナブルなコードが書けるようになります。

参考文献

  • TypeScript公式ハンドブック: https://www.typescriptlang.org/docs/handbook/
  • Effective TypeScript (Vanderkam, 2024) 2nd ed. O'Reilly.
  • Type Challenges: https://github.com/type-challenges/type-challenges
  • typescript-eslint: https://typescript-eslint.io/