07
📈 成長編 Chapter 07

型という盾

TypeScriptで安全なコードを書く

約50分
TypeScript · intro Type Safety · intro Interfaces · intro
目次(29セクション)
🎬 Story — Introduction

ShimaLinkのダッシュボードは動き出した。しかし、ある朝、Yukiから焦ったメッセージが届いた。


あなた: 「やばい、ダッシュボードが真っ白になった…昨日の夜、ちょっとコード直しただけなのに。」

あなた: 「エラーメッセージは?」

あなた: 「“Cannot read properties of undefined (reading ‘name’)“って。意味分からん…」

Yukiが画面を覗き込み、すぐに原因を見つけた。

Yuki: 「ここ。client.name を参照してるけど、clientがundefinedになってる。APIのレスポンス形式が変わったのに、コード側が追随してない。」

あなた: 「でも、前は動いてたんだよ!」

Yuki: 「JavaScriptは”自由”な言語だから、間違った型のデータが来てもコードを書いてる時点では教えてくれない。実行するまでバグが分からないんだ。」

あなた: 「じゃあ、どうすれば防げるんですか?」

Yuki: 「TypeScript。JavaScriptに”型”という盾を加えた言語だよ。コードを書いている時点でミスを教えてくれる。」


Yuki: 「TypeScriptは”JavaScriptのスーパーセット”——つまり、今書いてるJavaScriptのコードはそのまま動く。そこに型の情報を足していくだけ。怖がることはないよ。」

Yukiはターミナルを開き、TypeScriptのセットアップを始めた。ShimaLinkのコードをより安全にする旅が始まる。

なぜTypeScriptなのか

あなたのバグは、JavaScriptの「自由さ」が裏目に出た典型例だ。TypeScriptはその自由に「型」という秩序をもたらす。

「TypeScriptは交通ルールみたいなもの。なくても走れるけど、あった方が事故が減る。」——Yuki

JavaScriptの問題点

// JavaScriptでは何でも代入できる
let clientName = "海風テラス";
clientName = 42;        // エラーにならない!
clientName = { x: 1 };  // これもOK...

// 関数の引数に何が入るか分からない
function getClientInfo(client) {
  return client.name; // clientの形が保証されない
}

getClientInfo("文字列");  // 実行時エラー
getClientInfo(null);      // 実行時エラー

TypeScriptが解決すること

// TypeScriptでは型を明示する
let clientName: string = "海風テラス";
clientName = 42; // コンパイルエラー!「型'number'を型'string'に割り当てることはできません」

// 関数の引数と戻り値の型を宣言
function getClientInfo(client: { name: string }): string {
  return client.name;
}

getClientInfo("文字列"); // コンパイルエラー!
getClientInfo(null);     // コンパイルエラー!

TypeScriptのセットアップ

# プロジェクトにTypeScriptを追加
npm install -D typescript

# 設定ファイルを生成
npx tsc --init

# .tsファイルをコンパイル
npx tsc

tsconfig.json の基本設定

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

TypeScript = JavaScript + 型

特徴JavaScriptTypeScript
拡張子.js.ts
型チェックなし(実行時)あり(コンパイル時)
エラー発見実行時コード記述時
学習コスト低いやや高い
既存JSコードそのままそのまま動く

ポイント: TypeScriptはJavaScriptのスーパーセット。既存のJSファイルの拡張子を.tsに変えるだけで始められます。型は段階的に追加できるので、一気に全部書き換える必要はありません。

基本の型とアノテーション

TypeScriptの核心は「この変数にはどんな値が入るか」を宣言すること。これを型アノテーションと呼ぶ。

「型アノテーションは設計図の寸法。書いておけば、合わない部品を使おうとした瞬間に教えてくれる。」——Yuki

プリミティブ型

// 文字列
const clientName: string = "海風テラス";

// 数値
const monthlyVisits: number = 1250;

// 真偽値
const isActive: boolean = true;

// null と undefined
const deletedAt: null = null;
let memo: undefined = undefined;

型推論 — 書かなくても分かる場合

TypeScriptは代入された値から型を推論できます。

// 型推論が働くので、型アノテーションは省略可能
const name = "海風テラス";      // string と推論
const visits = 1250;            // number と推論
const active = true;            // boolean と推論

// ただし、関数の引数には明示するのがベスト
function greet(name: string): string {
  return `こんにちは、${name}さん!`;
}

配列の型

// 書き方1: 型[]
const clients: string[] = ["海風テラス", "首里そば太郎"];

// 書き方2: Array<型>
const visits: Array<number> = [1250, 890, 420];

// オブジェクトの配列
const clientData: { name: string; visits: number }[] = [
  { name: "海風テラス", visits: 1250 },
  { name: "首里そば太郎", visits: 890 }
];

オブジェクトの型

// インラインで型を定義
const client: {
  name: string;
  category: string;
  visits: number;
  isActive: boolean;
} = {
  name: "海風テラス",
  category: "カフェ",
  visits: 1250,
  isActive: true
};

関数の型

// 引数と戻り値に型をつける
function calculateTotal(prices: number[]): number {
  return prices.reduce((sum, price) => sum + price, 0);
}

// アロー関数
const formatClient = (name: string, visits: number): string => {
  return `${name}: ${visits}回`;
};

// 戻り値がない関数は void
function logMessage(message: string): void {
  console.log(message);
}

Union型 — 複数の型を許容

// string または number を受け付ける
let id: string | number;
id = "abc123";  // OK
id = 42;        // OK
id = true;      // エラー!

// null許容型
let phone: string | null = null;
phone = "098-XXX-XXXX"; // OK

リテラル型 — 特定の値のみ許容

// 特定の文字列だけ許可
type Category = "カフェ" | "飲食" | "花屋" | "雑貨";

const clientCategory: Category = "カフェ";    // OK
const invalid: Category = "レストラン";        // エラー!

any と unknown

// any: 型チェックを無効にする(非推奨)
let data: any = "hello";
data = 42;
data.foo.bar; // エラーにならない(危険!)

// unknown: anyの安全版(型チェックが必要)
let input: unknown = "hello";
// input.toUpperCase(); // エラー!型チェックが必要
if (typeof input === "string") {
  input.toUpperCase(); // OK(型が確認された)
}

まとめ

説明
string文字列"海風テラス"
number数値1250
boolean真偽値true
string[]文字列の配列["a", "b"]
A | BUnion型string | number
void戻り値なし関数の戻り値
unknown安全な不明型外部データ

ポイント: まずは関数の引数と戻り値に型をつけることから始めよう。それだけでバグの大半を防げます。

インターフェースと型エイリアス

毎回オブジェクトの型をインラインで書くのは大変だ。TypeScriptには、型に名前をつけて再利用する方法が2つある。

「interfaceはデータの”設計図”。チーム全員が同じ設計図を共有すれば、齟齬がなくなる。」——Yuki

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

// ShimaLinkのクライアント型を定義
interface Client {
  name: string;
  category: string;
  monthlyVisits: number;
  isActive: boolean;
}

// 使用する
const mika: Client = {
  name: "海風テラス",
  category: "カフェ",
  monthlyVisits: 1250,
  isActive: true
};

// 必須プロパティが欠けるとエラー
const invalid: Client = {
  name: "テスト",
  // エラー! category, monthlyVisits, isActive が不足
};

オプショナルプロパティ(?)

interface Client {
  name: string;
  category: string;
  monthlyVisits: number;
  isActive: boolean;
  phone?: string;       // あってもなくてもOK
  website?: string;     // あってもなくてもOK
}

const client: Client = {
  name: "海風テラス",
  category: "カフェ",
  monthlyVisits: 1250,
  isActive: true
  // phone, website は省略可能
};

読み取り専用(readonly)

interface Client {
  readonly id: string;  // 作成後は変更不可
  name: string;
  category: string;
}

const client: Client = {
  id: "client-001",
  name: "海風テラス",
  category: "カフェ"
};

client.name = "新しい名前"; // OK
client.id = "changed";     // エラー!readonlyプロパティは変更不可

type エイリアス

type キーワードでも型に名前をつけられます。

// type エイリアス
type ClientCategory = "カフェ" | "飲食" | "花屋" | "雑貨";

type Client = {
  name: string;
  category: ClientCategory;
  monthlyVisits: number;
  isActive: boolean;
};

interface vs type

特徴interfacetype
オブジェクト型得意可能
Union型不可"a" | "b"
拡張extends&
宣言マージ可能不可
プリミティブの別名不可可能
// interface の拡張(extends)
interface BaseClient {
  name: string;
  category: string;
}

interface PremiumClient extends BaseClient {
  monthlyFee: number;
  supportLevel: "standard" | "priority";
}

// type の交差型(&)
type BaseClient = {
  name: string;
  category: string;
};

type PremiumClient = BaseClient & {
  monthlyFee: number;
  supportLevel: "standard" | "priority";
};

実践: ShimaLinkの型定義

// types.ts — ShimaLinkの型定義ファイル
interface Client {
  readonly id: string;
  name: string;
  owner: string;
  category: ClientCategory;
  monthlyVisits: number;
  isActive: boolean;
  createdAt: string;
  phone?: string;
  website?: string;
}

type ClientCategory = "カフェ" | "飲食" | "花屋" | "雑貨" | "その他";

// APIレスポンスの型
interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
}

// 使用例
async function fetchClients(): Promise<ApiResponse<Client[]>> {
  const response = await fetch("/api/clients");
  return response.json();
}

関数の型定義

// 関数の型をinterfaceで定義
interface FormatFunction {
  (client: Client): string;
}

const formatClient: FormatFunction = (client) => {
  return `${client.name}(${client.category})`;
};

ポイント: 迷ったら interface を使おう。オブジェクトの形を定義するのに最適で、チーム開発では「データの契約書」として機能します。Union型が必要なときだけ type を使いましょう。

ジェネリクスの基礎 — 汎用的な型

APIレスポンスの型を定義するとき、データの中身はクライアント一覧だったり、予約一覧だったりする。毎回別の型を作るのは非効率だ。ここでジェネリクスが登場する。

「ジェネリクスは”型の変数”。中身は後から決めるけど、外側の構造は保証する。宅配ボックスみたいなものだね。」——Yuki

ジェネリクスとは

型を引数として受け取る仕組みです。<T> のように書きます。

// ジェネリクスなし → 型ごとに関数を作る必要がある
function getFirstClient(arr: Client[]): Client {
  return arr[0];
}

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

// ジェネリクスあり → 1つの関数でOK
function getFirst<T>(arr: T[]): T {
  return arr[0];
}

const firstClient = getFirst<Client>(clients);  // Client型
const firstNumber = getFirst<number>([1, 2, 3]); // number型
const firstString = getFirst(["a", "b"]);         // string型(推論)

APIレスポンスで活用

ShimaLinkのAPIは、どのエンドポイントも同じ形式でレスポンスを返す。中身だけが異なる。

// ジェネリクスなAPIレスポンス型
interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
  timestamp: string;
}

// クライアント一覧の取得
async function fetchClients(): Promise<ApiResponse<Client[]>> {
  const response = await fetch("/api/clients");
  return response.json();
}

// 単一クライアントの取得
async function fetchClient(id: string): Promise<ApiResponse<Client>> {
  const response = await fetch(`/api/clients/${id}`);
  return response.json();
}

// 予約一覧の取得
interface Reservation {
  id: string;
  clientId: string;
  date: string;
  guestName: string;
}

async function fetchReservations(): Promise<ApiResponse<Reservation[]>> {
  const response = await fetch("/api/reservations");
  return response.json();
}

制約つきジェネリクス(extends)

ジェネリクスに制約を加えて、受け付ける型を限定できます。

// T は必ず name プロパティを持つ
function getName<T extends { name: string }>(item: T): string {
  return item.name;
}

getName({ name: "海風テラス", visits: 1250 }); // OK
getName({ title: "テスト" }); // エラー!nameプロパティがない

よく使うユーティリティ型

TypeScriptには便利な組み込みジェネリクス型があります。

interface Client {
  id: string;
  name: string;
  category: string;
  visits: number;
}

// Partial<T> — すべてのプロパティをオプショナルに
type ClientUpdate = Partial<Client>;
// { id?: string; name?: string; category?: string; visits?: number }

// Required<T> — すべてのプロパティを必須に
type StrictClient = Required<Client>;

// Pick<T, K> — 特定のプロパティだけ取り出す
type ClientSummary = Pick<Client, "id" | "name">;
// { id: string; name: string }

// Omit<T, K> — 特定のプロパティを除外
type NewClient = Omit<Client, "id">;
// { name: string; category: string; visits: number }

実践的な使い方

// 新規作成時はidが不要
type CreateClientInput = Omit<Client, "id">;

// 更新時は部分的な変更でOK
type UpdateClientInput = Partial<Omit<Client, "id">>;

async function createClient(input: CreateClientInput): Promise<Client> {
  const response = await fetch("/api/clients", {
    method: "POST",
    body: JSON.stringify(input)
  });
  return response.json();
}

async function updateClient(
  id: string,
  input: UpdateClientInput
): Promise<Client> {
  const response = await fetch(`/api/clients/${id}`, {
    method: "PUT",
    body: JSON.stringify(input)
  });
  return response.json();
}

まとめ

概念説明
<T>型の引数function fn<T>(x: T): T
extends型の制約<T extends { id: string }>
Partial<T>全プロパティをオプショナルに更新処理
Pick<T, K>特定のプロパティだけ抽出一覧表示
Omit<T, K>特定のプロパティを除外新規作成

ポイント: ジェネリクスは最初は難しく感じるかもしれないが、ApiResponse<T> のように「外枠は同じで中身が変わる」パターンでまず慣れよう。

📖 Story — Conclusion

ShimaLinkのコードベースにTypeScriptを導入してから1週間。あなたがコードを書いていると、VSCodeが赤い波線でエラーを教えてくれるようになった。

// TypeScriptが事前に教えてくれる
const client: Client = await fetchClient(id);
client.nmae; // ← 赤い波線!「プロパティ'nmae'は型'Client'に存在しません。'name'ですか?」

あなた: 「すげえ、タイポまで見つけてくれるの?前みたいに実行して初めて気づくんじゃなくて。」

Yuki: 「でしょ?型があると、コードを書きながらバグを防げる。チームで開発するなら必須だよ。」

あなた: 「interfaceでデータの形を定義すると、APIから返ってくるデータの構造も保証できるんですね。」

Yuki: 「そう。でもね、今はまだフロントエンドの話だけ。クライアントが増えてきて、そろそろバックエンドが必要になる。」

あなた: 「バックエンド?」

Yuki: 「今、クライアントごとのデータは1つのJSONファイルに全部入ってる。でも、Mikaのカフェのデータと他のクライアントのデータを分けて管理する必要があるよね。それにはAPIが必要なんだ。」

あなた: 「API…聞いたことはあるけど、実際に作るとなると。」

Yuki: 「大丈夫。fetchで外部APIを使う方法はもう知ってる。今度は自分でAPIを作る側に回るだけだよ。」


次のチャプター: Chapter 8: APIという架け橋 — クライアントが増え、データの分離が必要に。RESTful APIの設計と構築を学ぶ。

🧠 理解度チェック

Q1.TypeScriptはJavaScriptに何を追加した言語?

💡 KentaのバグをYukiが「型があれば防げた」と言っていたのを思い出そう。

Q2.次のTypeScriptコードでエラーになるのはどれ? ``` let name: string = "Mika"; ```

💡 Kentaが型のない変数に意図しない値を代入してバグを起こした場面を思い出そう。

Q3.interfaceでオプショナル(任意)のプロパティを示す記号は?

💡 Client型の定義で、電話番号やWebサイトURLはあるクライアントとないクライアントがいるため、?をつけたのを覚えてる?

Q4.TypeScriptの型推論について正しいのは?

💡 Yukiが「const name = "海風テラス" だけでstringと分かる」と教えてくれた場面だね。

Q5.Partial<Client> はどのような型になる?

💡 クライアント情報を更新するとき、名前だけ変えたいのに全プロパティを渡すのは面倒。Partialを使えば必要な部分だけ指定できる。

Q6.ジェネリクスの<T>の役割は?

💡 ApiResponse<T>の<T>にClient[]を渡せばクライアント一覧、Reservation[]を渡せば予約一覧のレスポンス型になったね。

Q7.anyとunknownの違いは?

💡 Yukiが「anyは最終手段。まずunknownを試して、型ガードで安全にしよう」と言っていたのを思い出そう。

よくある質問

TypeScriptのファイルをブラウザで直接実行できない

ブラウザはTypeScript(`.ts`)を直接実行できません。JavaScriptに**コンパイル(変換)**する必要があります。 ```bash # TypeScriptコンパイラでJSに変換 npx tsc # 特定のファイルだけ変換 npx tsc src/app.ts # 監視モード(ファイル変更時に自動コンパイル) npx tsc --watch ``` 変換されたJSファイルは`outDir`(通常`dist/`)に出力されます。HTMLからは変換後の`.js`ファイルを読み込みます。 **代替手段**: ViteやNext.jsなどのビルドツールを使えば、自動的にTSをコンパイルしてくれます。

「Type 'X' is not assignable to type 'Y'」エラーの意味

**型の不一致**を示すエラーです。TypeScriptが最も多く出すエラーの一つです。 ```typescript // 例: string型にnumber型を代入しようとした let name: string = 42; // Error: Type 'number' is not assignable to type 'string' ``` **対処法**: 1. 値の型を修正する 2. 変数の型定義を見直す 3. Union型で複数の型を許容する: `string | number` **よくあるケース**: - APIレスポンスの型が実際のデータと合っていない - null/undefinedが考慮されていない - オブジェクトのプロパティが足りない

interfaceとtypeのどちらを使えばいい?

**基本方針**: オブジェクトの形を定義するなら`interface`、それ以外は`type`。 ```typescript // interface: オブジェクトの形 interface Client { name: string; visits: number; } // type: Union型、プリミティブの別名 type Category = "カフェ" | "飲食" | "花屋"; type ID = string; ``` **interfaceが有利な場面**: - クラスのimplements - 宣言マージ(後から拡張可能) - エラーメッセージが読みやすい **typeが有利な場面**: - Union型やIntersection型 - タプル型 - 関数型の定義

「Property 'X' does not exist on type 'Y'」の対処法

型定義に存在しないプロパティにアクセスしようとしています。 ```typescript interface Client { name: string; } const client: Client = { name: "海風テラス" }; client.phone; // Error! Clientにphoneは定義されていない ``` **対処法**: 1. **型定義にプロパティを追加する** ```typescript interface Client { name: string; phone?: string; // オプショナルで追加 } ``` 2. **タイポを修正する**(VSCodeが候補を表示してくれます) 3. **型ガードを使う**(unknownからアクセスする場合)

ジェネリクスの<T>が何を意味するのか分からない

`<T>`は**型の変数**(プレースホルダー)です。 普通の変数が「値」を受け取るように、ジェネリクスは「型」を受け取ります。 ```typescript // 普通の関数: 値を受け取る function echo(value: string): string { return value; } // ジェネリクス関数: 型も受け取る function echo<T>(value: T): T { return value; } // 使うとき echo<string>("hello"); // T = string echo<number>(42); // T = number echo("auto"); // T = string(推論) ``` **例え**: 宅配ボックスの形は決まっているけど、中に何を入れるかは後から決める。`T`は「中身の種類」を後から指定する仕組みです。

strictモードを有効にしたらエラーが大量に出た

`strict: true`は複数の厳格チェックを一括で有効にします。段階的に対応しましょう。 **strictに含まれるチェック**: - `strictNullChecks`: null/undefinedの厳格チェック - `noImplicitAny`: 暗黙のany禁止 - `strictFunctionTypes`: 関数型の厳格チェック **段階的な対応方法**: ```json // tsconfig.json で個別に有効化 { "compilerOptions": { "strict": false, "strictNullChecks": true, "noImplicitAny": true } } ``` まず`noImplicitAny`から始めて、慣れたら`strictNullChecks`を追加するのがおすすめです。

既存のJavaScriptプロジェクトにTypeScriptを導入するには?

一気に全部書き換える必要はありません。段階的に導入できます。 **ステップ1**: TypeScriptをインストール ```bash npm install -D typescript npx tsc --init ``` **ステップ2**: `allowJs: true` を設定 ```json { "compilerOptions": { "allowJs": true, "checkJs": false } } ``` **ステップ3**: 1ファイルずつ`.js`を`.ts`にリネーム **ステップ4**: 型アノテーションを追加 **コツ**: 新しく作るファイルは最初から`.ts`で作り、既存ファイルは時間があるときに順次変換する。

Union型のnarrowingとは?

**Narrowing(型の絞り込み)**は、Union型から特定の型を確定する処理です。 ```typescript function display(value: string | number) { // ここでは value は string | number if (typeof value === "string") { // ここでは value は string と確定 console.log(value.toUpperCase()); } else { // ここでは value は number と確定 console.log(value.toFixed(2)); } } ``` **narrowingの方法**: - `typeof`: プリミティブ型の判定 - `instanceof`: クラスの判定 - `in`: プロパティの存在チェック - `===`: リテラル型の判定 TypeScriptは条件分岐の中で自動的に型を絞り込んでくれます。