ウェブエンジニア問題集
第14章

実践パターン集 — よくある型の書き方

4
この章の目次開く

この章は逆引きリファレンスです。「こういうことがしたいとき、型をどう書くか」を場面ごとにまとめます。

学習者学習者

やりたいことは分かってるのに、型の書き方だけ思い出せない…ってよくあるんだよね。

APIレスポンスの型定義

APIの成功・失敗を型で表現するパターン。

// 成功レスポンス
type ApiResponse<T> = {
  data: T;
  meta: { total: number; page: number; perPage: number };
};
 
// エラーレスポンス
type ErrorResponse = {
  error: { code: string; message: string };
};
 
// 成功 or 失敗のユニオン型
type ApiResult<T> =
  | { ok: true; data: T; meta: ApiResponse<T>['meta'] }
  | { ok: false; error: ErrorResponse['error'] };
tsx
// 使用例 — fetch のラッパー関数
async function fetchApi<T>(url: string): Promise<ApiResult<T>> {
  const res = await fetch(url);
  if (!res.ok) {
    const err = await res.json();
    return { ok: false, error: err.error };
  }
  const json = await res.json();
  return { ok: true, data: json.data, meta: json.meta };
}
 
// 呼び出し側 — 型が絞り込まれる
const result = await fetchApi<User[]>('/api/users');
if (result.ok) {
  console.log(result.data);  // User[]
  console.log(result.meta.total);
} else {
  console.error(result.error.message);  // string
}
tsx
APIの成功・失敗を discriminated union で表現すると、呼び出し側で if (result.ok) だけで型が絞り込まれる。

フォームの状態管理

type LoginForm = {
  email: string;
  password: string;
};
 
// フィールドごとに更新できるハンドラ
function useForm<T extends Record<string, unknown>>(initialValues: T) {
  const [values, setValues] = useState(initialValues);
 
  const handleChange = (field: keyof T, value: T[keyof T]) => {
    setValues((prev) => ({ ...prev, [field]: value }));
  };
 
  const reset = () => setValues(initialValues);
 
  return { values, handleChange, reset };
}
 
// 使用例
const { values, handleChange } = useForm<LoginForm>({
  email: '',
  password: '',
});
 
// handleChange('email', 'test@example.com')  ✅
// handleChange('typo', 'value')               ❌ コンパイルエラー
tsx

環境変数の型付け

process.env の型を安全に扱うには、型定義ファイルで拡張します。

// env.d.ts(プロジェクトルートに置く)
declare namespace NodeJS {
  interface ProcessEnv {
    DATABASE_URL: string;
    JWT_SECRET: string;
    NEXT_PUBLIC_API_URL: string;
    NODE_ENV: 'development' | 'production' | 'test';
  }
}
tsx
// これで process.env.DATABASE_URL が string 型として補完される
const dbUrl = process.env.DATABASE_URL;  // string
 
// 存在しない環境変数はエラーになる
// const x = process.env.TYPO;  // ❌ プロパティが存在しない
tsx

配列から型を抽出する(as const)

const statuses = ['idle', 'loading', 'success', 'error'] as const;
type Status = (typeof statuses)[number];
// 'idle' | 'loading' | 'success' | 'error'
 
// 配列とユニオン型を同時に使える
function isValidStatus(value: string): value is Status {
  return (statuses as readonly string[]).includes(value);
}
tsx
先生先生

as const を付けないと string[] に推論されてしまい、ユニオン型が取れない。as const は「この配列はリテラル型の読み取り専用タプルだ」と宣言するもの。

オブジェクトのキーから型を作る

const routes = {
  home: '/',
  about: '/about',
  contact: '/contact',
} as const;
 
type RouteName = keyof typeof routes;
// 'home' | 'about' | 'contact'
 
type RoutePath = (typeof routes)[RouteName];
// '/' | '/about' | '/contact'
 
// 型安全なナビゲーション関数
function navigate(name: RouteName) {
  const path = routes[name];
  window.location.href = path;
}
 
navigate('home');    // ✅
// navigate('typo'); // ❌ コンパイルエラー
tsx

型の網羅性チェック(exhaustive check)

switch文で全ケースを処理したことをコンパイラに保証させるパターン。

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number }
  | { kind: 'triangle'; base: number; height: number };
 
function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'square':
      return shape.side ** 2;
    case 'triangle':
      return (shape.base * shape.height) / 2;
    default: {
      const _exhaustive: never = shape;
      return _exhaustive;
    }
  }
}
tsx
将来 Shape にバリアントを追加したら、never に代入できなくなりコンパイルエラーで教えてくれる。

Mapped Types — 既存の型を変換する

// すべてのプロパティをオプショナルにする(Partial の内部実装)
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};
 
// すべてのプロパティを読み取り専用にする(Readonly の内部実装)
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};
 
// 特定のキーだけ必須にする
type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;
 
type User = { name?: string; email?: string; age?: number };
type UserWithEmail = RequireKeys<User, 'email'>;
// email は必須、name と age はオプショナルのまま
tsx

Template Literal Types

文字列パターンを型で表現できます。

type EventName = `on${Capitalize<'click' | 'change' | 'submit'>}`;
// 'onClick' | 'onChange' | 'onSubmit'
 
type CSSProperty = `${string}-${string}`;
// 'background-color', 'font-size' など
tsx
// 実務的な例: APIのエンドポイント型
type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'products' | 'orders';
type Endpoint = `/api/${ApiVersion}/${Resource}`;
// '/api/v1/users' | '/api/v1/products' | '/api/v1/orders'
// | '/api/v2/users' | '/api/v2/products' | '/api/v2/orders'
tsx

Conditional Types

ライブラリの型定義を読むときに出てくるパターン。

// 基本形
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<string>;   // 'yes'
type B = IsString<number>;   // 'no'
 
// infer で型を抽出する
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
 
type Fn = (x: number) => string;
type Result = ReturnType<Fn>;  // string
tsx
// Promise の中身を取り出す
type Awaited<T> = T extends Promise<infer U> ? U : T;
 
type P = Awaited<Promise<string>>;  // string
type Q = Awaited<string>;           // string(Promiseでないならそのまま)
tsx
型パターンの活用
これらのパターンは丸暗記せず、必要なときにこのページを参照する使い方で十分

ちゃんと使うためのポイント

  • APIレスポンスは discriminated union で成功・失敗を表現する
  • as const で配列やオブジェクトからリテラル型を抽出できる
  • 環境変数は env.d.ts で型を付けるが、実行時の存在は別途バリデーション
  • exhaustive check で switch 文の網羅性をコンパイラに保証させる
  • Mapped Types / Conditional Types はライブラリの型定義を読むときに知っていると強い

参考リンク

TypeScriptクイズに挑戦するこの章で学んだTypeScriptの知識を、4択クイズでアウトプットして定着させよう