型安全で読みやすく、保守しやすい TypeScript コードを書くための実践的なガイドです。


目次

  1. コンパイラ設定(tsconfig)
  2. 型の基本方針
  3. any / unknown / never の使い分け
  4. 型定義:type と interface
  5. ユニオン型と型の絞り込み
  6. null / undefined の扱い
  7. 関数の設計
  8. ジェネリクス
  9. ユーティリティ型の活用
  10. イミュータビリティ
  11. enum の代替
  12. エラーハンドリング
  13. 非同期処理
  14. 外部データの検証
  15. モジュールとインポート
  16. クラスとオブジェクト指向
  17. 命名規則
  18. ツールと品質管理
  19. 避けるべきアンチパターン
  20. チェックリスト

1. コンパイラ設定(tsconfig)

strict: true は必須。 新規プロジェクトでは最初から有効にしましょう。後から有効にすると大量のエラー修正が必要になります。

{
  "compilerOptions": {
    // --- 型チェックの厳格化 ---
    "strict": true,                          // strictNullChecks, noImplicitAny などを一括有効化
    "noUncheckedIndexedAccess": true,        // arr[i] / obj[key] を T | undefined として扱う
    "exactOptionalPropertyTypes": true,      // `prop?: T` に undefined を明示代入するのを禁止
    "noImplicitOverride": true,              // オーバーライド時に override キーワードを必須化
    "noImplicitReturns": true,               // すべてのコードパスで return を要求
    "noFallthroughCasesInSwitch": true,      // switch の fallthrough を禁止
    "noPropertyAccessFromIndexSignature": true,
 
    // --- モジュール ---
    "module": "NodeNext",                    // バンドラ利用時は "ESNext" + "moduleResolution": "Bundler"
    "moduleResolution": "NodeNext",
    "verbatimModuleSyntax": true,            // import type を強制し、出力を予測可能に
    "isolatedModules": true,                 // esbuild / swc などの単一ファイル変換と互換に
    "esModuleInterop": true,
    "resolveJsonModule": true,
 
    // --- 出力 ---
    "target": "ES2022",
    "sourceMap": true,
    "declaration": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

💡 Node.js の型ストリップ機能などで「型を消すだけ」で実行する場合は "erasableSyntaxOnly": true を有効にすると、enum や namespace などランタイムコードを生む構文を禁止できます。


2. 型の基本方針

型推論に任せられるところは任せる

// ❌ 冗長
const count: number = 0;
const names: string[] = ["a", "b"];
 
// ✅ 推論に任せる
const count = 0;
const names = ["a", "b"];

境界(公開 API)には明示的に型を書く

関数の引数・戻り値、エクスポートする値には型注釈を付けます。意図しない型変更が外部に漏れるのを防げます。

// ✅ 公開関数の戻り値は明示する
export function parseUser(input: string): User {
  // ...
}

satisfies で「型チェック」と「推論の保持」を両立する

type Route = { path: string; auth: boolean };
 
// ❌ 型注釈だとキーの情報が失われる
const routes: Record<string, Route> = {
  home: { path: "/", auth: false },
};
routes.typo; // エラーにならない
 
// ✅ satisfies なら型チェックしつつ具体的な型を保持
const routes = {
  home: { path: "/", auth: false },
  admin: { path: "/admin", auth: true },
} satisfies Record<string, Route>;
routes.typo; // ❌ コンパイルエラー

as const でリテラル型を保持する

const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"

3. any / unknown / never の使い分け

型用途方針
any型チェックを無効化原則使わない。移行期の一時的な逃げ道のみ
unknown型が分からない値外部入力・catch の例外などに使い、絞り込んでから利用
never起こり得ない値網羅性チェックや到達不能コードに使う
// ❌ any は型安全性を破壊し、伝播する
function handle(data: any) {
  data.foo.bar(); // 実行時エラーの可能性
}
 
// ✅ unknown は使う前に絞り込みが必要
function handle(data: unknown) {
  if (typeof data === "object" && data !== null && "foo" in data) {
    // ...
  }
}

型アサーション(as)は最小限に

as はコンパイラに「信じて」と言うだけで、実行時の保証はありません。型ガードやバリデーションで置き換えましょう。

// ❌
const user = JSON.parse(text) as User;
 
// ✅ 検証してから使う(→ 14章参照)
const user = UserSchema.parse(JSON.parse(text));

非 null アサーション(!)も同様に、可能な限り避けます。


4. 型定義:type と interface

どちらでも多くのことができます。チーム内で一貫性を保つことが最重要です。

一般的な指針:

  • type — ユニオン型、交差型、マップ型、条件型、タプルなど、interface で表現できないもの
  • interface — オブジェクトの形状、特に拡張(extends)や宣言マージが必要な公開 API
// type が必要なケース
type Status = "idle" | "loading" | "success" | "error";
type Pair<T> = [T, T];
 
// interface が適するケース
interface User {
  id: string;
  name: string;
}
 
interface Admin extends User {
  permissions: string[];
}

interface の extends は交差型(&)よりもエラーメッセージが分かりやすく、型チェックのパフォーマンスも良い傾向があります。


5. ユニオン型と型の絞り込み

判別可能なユニオン(Discriminated Union)を使う

状態を表すときは、オプショナルなプロパティを並べるのではなく、共通の判別キーを持つユニオンにします。

// ❌ 不正な状態が表現できてしまう(loading なのに data がある等)
type State = {
  loading: boolean;
  data?: User;
  error?: Error;
};
 
// ✅ 不正な状態を型レベルで排除
type State =
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: Error };

網羅性チェック(Exhaustiveness Check)

function render(state: State): string {
  switch (state.status) {
    case "loading":
      return "Loading...";
    case "success":
      return state.data.name;
    case "error":
      return state.error.message;
    default: {
      const _exhaustive: never = state; // ケース追加漏れでコンパイルエラー
      return _exhaustive;
    }
  }
}

ユーザー定義型ガード

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    typeof value.id === "string"
  );
}

6. null / undefined の扱い

  • strictNullChecks(strict に含まれる)を必ず有効に
  • オプショナルチェーン ?. と Null 合体演算子 ?? を活用
  • || ではなく ?? を使う(0 や "" を誤って置き換えないため)
// ❌ count が 0 のとき 10 になってしまう
const value = count || 10;
 
// ✅ null / undefined のときだけデフォルト値
const value = count ?? 10;
 
const city = user?.address?.city ?? "不明";
  • プロジェクト内で null と undefined のどちらを「値がない」の表現に使うか統一すると混乱が減ります

7. 関数の設計

引数が多い場合はオブジェクトで受け取る

// ❌ 引数の順番を間違えやすい
function createUser(name: string, email: string, age: number, isAdmin: boolean) {}
 
// ✅ 名前付き引数のように使える
function createUser(params: {
  name: string;
  email: string;
  age: number;
  isAdmin?: boolean;
}) {}

boolean フラグ引数を避ける

// ❌ 呼び出し側で意味が分からない
fetchUsers(true);
 
// ✅
fetchUsers({ includeInactive: true });

引数は広く、戻り値は狭く

// 引数は readonly で受けると、可変・不変どちらの配列も渡せる
function sum(values: readonly number[]): number {
  return values.reduce((a, b) => a + b, 0);
}

オーバーロードよりユニオンや条件型を検討

オーバーロードは複雑になりがちです。ユニオン型で表現できるならそちらを優先します。


8. ジェネリクス

意味のある制約を付ける

// ❌ 制約がなく、T について何も分からない
function getProp<T>(obj: T, key: string) {}
 
// ✅ キーの存在を型で保証
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

不要なジェネリクスを作らない

型パラメータが 1 回しか登場しないなら、ジェネリクスは不要なことが多いです。

// ❌ T が引数に1回しか出てこない
function log<T>(value: T): void {
  console.log(value);
}
 
// ✅
function log(value: unknown): void {
  console.log(value);
}

型パラメータ名

単純なものは T, K, V で十分ですが、複数あるときは TInput, TOutput のように意味のある名前を付けます。


9. ユーティリティ型の活用

既存の型から派生させることで、定義の重複と不整合を防ぎます。

ユーティリティ型説明
Partial<T>すべてのプロパティをオプショナルに
Required<T>すべてのプロパティを必須に
Readonly<T>すべてのプロパティを読み取り専用に
Pick<T, K>指定したプロパティだけを抽出
Omit<T, K>指定したプロパティを除外
Record<K, V>キー K、値 V のオブジェクト型
Exclude<T, U> / Extract<T, U>ユニオンから除外 / 抽出
NonNullable<T>null と undefined を除外
ReturnType<F> / Parameters<F>関数の戻り値 / 引数の型
Awaited<T>Promise を解決した型
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}
 
type CreateUserInput = Omit<User, "id" | "createdAt">;
type UpdateUserInput = Partial<CreateUserInput>;
type UserPreview = Pick<User, "id" | "name">;

10. イミュータビリティ

interface Config {
  readonly apiUrl: string;
  readonly retries: number;
}
 
function process(items: readonly string[]) {
  items.push("x"); // ❌ コンパイルエラー
}
 
// 新しい配列を返すメソッドを優先
const sorted = items.toSorted();   // sort() は元配列を変更する
const reversed = items.toReversed();
const updated = items.with(0, "new");
  • 基本は const、再代入が必要なときだけ let。var は使わない
  • 状態更新はスプレッド構文などで新しいオブジェクトを作る

11. enum の代替

enum はランタイムコードを生成し、数値 enum は型安全性に穴もあります。多くの場合、ユニオン型 または as const オブジェクトで代替できます。

// ❌ enum
enum Direction {
  Up,
  Down,
}
 
// ✅ ユニオン型(最もシンプル)
type Direction = "up" | "down";
 
// ✅ 値の一覧も必要なら as const オブジェクト
const Direction = {
  Up: "up",
  Down: "down",
} as const;
type Direction = (typeof Direction)[keyof typeof Direction]; // "up" | "down"

12. エラーハンドリング

catch の例外は unknown として扱う

strict 下では catch (e) の e は unknown です(useUnknownInCatchVariables)。

try {
  await save();
} catch (error) {
  if (error instanceof Error) {
    logger.error(error.message);
  } else {
    logger.error("Unknown error", { error });
  }
}

独自エラークラスで原因を区別する

class NotFoundError extends Error {
  override readonly name = "NotFoundError";
  constructor(public readonly resource: string, options?: ErrorOptions) {
    super(`${resource} not found`, options);
  }
}
 
// cause でエラーの連鎖を保持
throw new NotFoundError("User", { cause: originalError });

想定内の失敗は Result 型で表現する

例外は「想定外」の失敗に、想定内の失敗(バリデーションエラーなど)は戻り値で表現すると、呼び出し側が処理を強制されます。

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };
 
function parseAge(input: string): Result<number, string> {
  const n = Number(input);
  if (!Number.isInteger(n) || n < 0) {
    return { ok: false, error: "年齢は0以上の整数で入力してください" };
  }
  return { ok: true, value: n };
}

13. 非同期処理

  • コールバックより async / await を使う
  • Promise を放置しない(ESLint の no-floating-promises で検出)
  • 独立した処理は並列化する
// ❌ 直列実行で遅い
const user = await fetchUser(id);
const posts = await fetchPosts(id);
 
// ✅ 並列実行
const [user, posts] = await Promise.all([fetchUser(id), fetchPosts(id)]);
 
// 一部の失敗を許容するなら allSettled
const results = await Promise.allSettled(tasks);
  • forEach の中で await しない(待たれない)。for...of か Promise.all + map を使う
  • キャンセルが必要な処理には AbortSignal を受け取れるようにする

14. 外部データの検証

TypeScript の型はコンパイル時にしか存在しません。 API レスポンス、フォーム入力、環境変数、JSON.parse の結果などは実行時に検証が必要です。

スキーマバリデーションライブラリ(Zod、Valibot、ArkType など)を使うと、スキーマから型を導出でき、定義の二重管理を防げます。

import { z } from "zod";
 
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().int().nonnegative().optional(),
});
 
type User = z.infer<typeof UserSchema>; // スキーマから型を導出
 
async function getUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return UserSchema.parse(await res.json()); // 不正なら例外
}

環境変数も起動時にまとめて検証しておくと、設定ミスを早期に発見できます。


15. モジュールとインポート

  • ES Modules(import / export)を使う。 namespace や require は避ける
  • 名前付きエクスポートを優先する。 デフォルトエクスポートは名前がぶれやすく、リファクタリングや自動インポートと相性が悪い
  • 型のみのインポートは import type を使う(verbatimModuleSyntax で強制可能)
import type { User } from "./types";
import { createUser, type CreateUserInput } from "./user";
  • 巨大なバレルファイル(index.ts で全部を re-export)は、循環参照やビルド・テストの遅延の原因になるため注意
  • パスエイリアス(@/components など)を使う場合は、ビルドツール側の設定と揃える

16. クラスとオブジェクト指向

  • クラスが本当に必要か検討する。状態を持たないなら関数とモジュールで十分なことが多い
  • アクセス修飾子を明示する。真に非公開にしたいなら #private フィールド(ランタイムでも非公開)
  • 継承より合成(コンポジション)を優先する
  • 依存はコンストラクタで注入し、テストしやすくする
class UserService {
  readonly #repo: UserRepository;
 
  constructor(repo: UserRepository) {
    this.#repo = repo;
  }
 
  async findById(id: string): Promise<User | undefined> {
    return this.#repo.find(id);
  }
}

17. 命名規則

対象規則例
変数・関数camelCaseuserName, fetchUser()
型・interface・クラスPascalCaseUser, UserService
定数(モジュールレベル)UPPER_SNAKE_CASE または camelCaseMAX_RETRIES
booleanis / has / can / should 接頭辞isActive, hasPermission
ファイル名プロジェクトで統一(kebab-case が一般的)user-service.ts
  • interface に I 接頭辞(IUser)は付けないのが TypeScript では一般的
  • 略語を避け、意図が伝わる名前にする

18. ツールと品質管理

Lint とフォーマット

  • ESLint + typescript-eslint:型情報を使ったルール(recommendedTypeChecked / strictTypeChecked)を有効にする
  • Prettier または Biome:フォーマットは自動化し、レビューで議論しない

特に有用な typescript-eslint ルール:

  • no-explicit-any
  • no-floating-promises / no-misused-promises
  • no-unnecessary-condition
  • switch-exhaustiveness-check
  • consistent-type-imports
  • prefer-nullish-coalescing / prefer-optional-chain

CI での型チェック

esbuild、swc、Vite などのトランスパイラは型チェックをしません。CI で必ず型チェックを実行します。

// package.json
{
  "scripts": {
    "typecheck": "tsc --noEmit",
    "lint": "eslint .",
    "test": "vitest run"
  }
}

テスト

  • Vitest や Jest でユニットテストを書く
  • 型そのもののテストには expectTypeOf(Vitest)や @ts-expect-error を使う
// 意図的にエラーになることをテスト
// @ts-expect-error - 数値は受け付けない
createUser({ name: 123 });

@ts-ignore ではなく @ts-expect-error を使いましょう。エラーが解消されたときに通知されます。


19. 避けるべきアンチパターン

アンチパターン問題点代替
any の多用型安全性が失われ、伝播するunknown + 絞り込み、ジェネリクス
as の乱用実行時の保証がない型ガード、スキーマ検証
!(非 null アサーション)null の可能性を握りつぶす?.、??、早期 return
@ts-ignoreエラーを隠す根本修正、やむを得なければ @ts-expect-error + 理由コメント
Function / Object / {} 型ほぼ何でも受け入れる具体的なシグネチャ、Record<string, unknown>
ラッパー型 String / Numberプリミティブと異なるstring / number
型定義の重複不整合の原因ユーティリティ型やスキーマから導出
オプショナルだらけの状態型不正な状態を許す判別可能なユニオン
数値 enum任意の数値を受け入れてしまうユニオン型、as const
過度に複雑な型パズル可読性・コンパイル速度の低下シンプルな型、必要ならコメントで意図を説明

20. チェックリスト

プロジェクト設定

  • strict: true が有効
  • noUncheckedIndexedAccess が有効
  • verbatimModuleSyntax / isolatedModules が有効
  • ESLint(typescript-eslint の型チェック付きルール)を導入
  • フォーマッタを導入し自動実行
  • CI で tsc --noEmit を実行

コードレビュー時

  • any / as / ! / @ts-ignore が正当な理由なく使われていない
  • 外部データは実行時に検証されている
  • 状態は判別可能なユニオンで表現され、網羅性チェックがある
  • 公開関数の引数・戻り値に型が明示されている
  • 型定義が重複せず、既存の型から導出されている
  • Promise が放置されていない
  • || ではなく ?? を使うべき箇所がない
  • 名前から意図が分かる

参考資料

0件のページ