TypeScriptには豊富な型システムがありますが、生産的になるためにすべてを理解する必要はありません。現実には、毎日使う少数の機能で作業の90%が完了します。これは実践的なリファレンスです — プリミティブ、ユニオン、ジェネリクス、ユーティリティ型、そしてすべてを理解させる絞り込みパターン。抽象的な理論なし;すべての例は実際のプロジェクトで書くコードです。JavaScriptをよく知っていて、TypeScriptをある程度やったことがあるが、メンタルモデルを固めたい場合はこれがあなたのためです。完全な話は TypeScriptハンドブックにあります — この記事は80/20版です。

プリミティブとリテラル

TypeScriptのプリミティブ型はJavaScriptのランタイム型に直接対応します: stringnumberbooleannullundefinedbigintsymbol。最初の3つは常に使います;残りは特定のコンテキストで出てきます。

ts
// Primitives — the foundation
let userId: number = 42;
let username: string = 'alice';
let isActive: boolean = true;
let deletedAt: Date | null = null;     // nullable pattern
let refreshToken: string | undefined;  // optional pattern

TypeScriptが面白くなるのはリテラル型です。値が単にstringであると言う代わりに、特定の文字列セットの1つでなければならないと言えます。これはTypeScriptがコンパイル時に無効な値を検出できるため、普通のstringアノテーションよりずっと役立ちます:

ts
type Direction = 'north' | 'south' | 'east' | 'west';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
type StatusCode = 200 | 201 | 400 | 401 | 403 | 404 | 500;

function navigate(dir: Direction) {
  console.log(`Moving ${dir}`);
}

navigate('north');  // ✅
navigate('up');     // ❌ Argument of type '"up"' is not assignable to type 'Direction'

// Combine with string for "known values plus free-form"
type EventName = 'click' | 'focus' | 'blur' | (string & {});
最後の(string & {})トリックは、既知の値のオートコンプリートを維持しながら任意の文字列も受け入れます。デザインシステムライブラリでよく見られるパターンです。

interfaceとtype — どちらをいつ使うか

これはすべてのTypeScript初心者が質問します。実践的な答え:オブジェクトの形状にはinterfaceを使います — 特にパブリックAPIコントラクトを表すもの、または他の型が拡張するもの。ユニオン、インターセクション、マップ型、および純粋なオブジェクト形状ではないものにはtypeを使います。実際には、オブジェクト形状においてはほぼ互換性があります — 1つの規則を選んでコードベース内で一貫性を保ってください。

ts
// interface — object shapes, extensible via extends
interface User {
  id: number;
  email: string;
  createdAt: Date;
}

interface AdminUser extends User {
  role: 'admin';
  permissions: string[];
}

// type — unions, intersections, aliases for anything
type ID = string | number;
type Nullable<T> = T | null;

// Intersection — combine two shapes (common for mixins / HOC props)
type WithTimestamps = {
  createdAt: Date;
  updatedAt: Date;
};

type UserRecord = User & WithTimestamps;

// type is required here — interface can't express a union of shapes
type ApiResponse<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; error: string; code: number };

意味のある違い:インターフェースは宣言マージをサポートします — 同じインターフェースを2回宣言すると、TypeScriptは定義をマージします。これがライブラリがグローバル型を拡張する方法(例:Windowにプロパティを追加)です。型はマージしません;型を再宣言するとエラーになります。

ユニオン型とインターセクション型

ユニオン型(A | B)は「この値はAまたはBです」と言います。インターセクション型 (A & B)は「この値は同時にAとBの両方です」と言います。ユニオンは実際のコードの至る所にあります — それらが可能にする最も強力なパターンは判別共用体で、これはオプションフィールドを至る所に使うことなく、異なる状態にあることができるデータをモデル化する方法です。

ts
// Discriminated union — model API response states cleanly
type FetchState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string; retryable: boolean };

// TypeScript narrows the type inside each branch
function render<T>(state: FetchState<T>) {
  switch (state.status) {
    case 'idle':    return '<p>Not started</p>';
    case 'loading': return '<p>Loading...</p>';
    case 'success': return `<pre>${JSON.stringify(state.data)}</pre>`;
    case 'error':   return `<p>Error: ${state.error}</p>`;
  }
}

// Intersection — common in React/Angular for composing prop types
type ButtonBaseProps = {
  label: string;
  disabled?: boolean;
};

type IconButtonProps = ButtonBaseProps & {
  icon: string;
  iconPosition: 'left' | 'right';
};

判別共用体パターンは判別子に依存します — 各バリアントに固有のリテラルプロパティ(ここではstatus)です。TypeScriptはそのプロパティを使用して条件分岐内で完全な型を絞り込み、dataerrorのようなバリアント固有のフィールドへの型安全なアクセスを提供します。

ジェネリクス — 全員を躓かせる部分

動機となる問題:複数の型で機能する関数が欲しいが、anyを使うとすべての型情報が失われます。ジェネリクスはこれを解決します — 山括弧で書かれる型パラメータで、関数やインターフェースが完全な型安全性を維持しながら型のファミリーで動作することを可能にします。

ts
// Without generics — any loses all information
function first(arr: any[]): any {
  return arr[0];
}
const x = first([1, 2, 3]); // x is 'any' — TypeScript can't help you here

// With generics — T is inferred from the argument
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
const n = first([1, 2, 3]);       // n: number | undefined ✅
const s = first(['a', 'b', 'c']); // s: string | undefined ✅

<T>構文は型パラメータを宣言します。何でも名前を付けられます — Tは単一のジェネリック型の慣例にすぎません。関数を呼び出すと、TypeScriptは引数からTを推論するので、明示的に書く必要はほとんどありません。ジェネリックインターフェースはAPIの形状をモデル化するのにも同様に役立ちます:

ts
// Generic interface — the API wrapper pattern every codebase has
interface ApiResponse<T> {
  data: T;
  meta: {
    page: number;
    totalPages: number;
    totalItems: number;
  };
}

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

// The response type is fully typed — no casting needed
async function fetchUsers(): Promise<ApiResponse<User[]>> {
  const res = await fetch('/api/users');
  return res.json();
}

const response = await fetchUsers();
response.data[0].email; // ✅ TypeScript knows this is a string
response.meta.totalPages; // ✅ TypeScript knows this is a number

// Constrained generics — T must have an 'id' field
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

const user = findById(users, 42); // T inferred as User
ジェネリクスをいつ使うか:異なる型で同じ関数シグネチャを2回書いていることに気づいたとき、または望む戻り値の型を得るためにasでキャストしているとき。それらはジェネリクスの方がよりクリーンだという2つのサインです。

実際に使うユーティリティ型

TypeScriptには既存の型を新しい型に変換する組み込みの ユーティリティ型セットが付属しています。型定義を手動で複製または調整する必要をなくします。実際のコードベースで常に出てくるものを紹介します:

ts
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
  createdAt: Date;
}

// Partial<T> — all properties become optional
// Perfect for PATCH/update payloads
type UpdateUserPayload = Partial<User>;
// { id?: number; name?: string; email?: string; ... }

async function updateUser(id: number, payload: Partial<User>) {
  return fetch(`/api/users/${id}`, {
    method: 'PATCH',
    body: JSON.stringify(payload)
  });
}

// Required<T> — all properties become required (opposite of Partial)
interface Config {
  apiUrl?: string;
  timeout?: number;
  retries?: number;
}
type ResolvedConfig = Required<Config>;
// { apiUrl: string; timeout: number; retries: number }

// Pick<T, K> — keep only specified properties
type UserSummary = Pick<User, 'id' | 'name' | 'email'>;
// Use in list views where you only need the display fields

// Omit<T, K> — exclude specified properties
type PublicUser = Omit<User, 'role' | 'createdAt'>;
// Safe to expose in API responses

// Readonly<T> — prevents mutation (great for config and frozen state)
const config: Readonly<ResolvedConfig> = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};
// config.timeout = 10000; // ❌ Cannot assign to 'timeout' because it is a read-only property

// Record<K, V> — typed dictionary
type UserCache = Record<number, User>;
const cache: UserCache = {};
cache[42] = { id: 42, name: 'Alice', email: '[email protected]', role: 'user', createdAt: new Date() };

// Common pattern: mapping string keys to a known value shape
type FeatureFlags = Record<string, { enabled: boolean; rolloutPct: number }>;

// ReturnType<T> — infer the return type of a function
function createSession(userId: number) {
  return {
    token: crypto.randomUUID(),
    userId,
    expiresAt: new Date(Date.now() + 86_400_000)
  };
}

type Session = ReturnType<typeof createSession>;
// { token: string; userId: number; expiresAt: Date }
// — no need to define the type separately and keep it in sync

unknownとanyとnever

これら3つの型はほとんどの開発者を混乱させます。anyはエスケープハッチです — その値の型チェックを完全に無効にします。JavaScriptからTypeScriptに移行するときに役立ちますが、過度な使用はTypeScriptの目的を損ないます。unknownは型安全な代替手段:値が何でもある可能性がありますが、それを使う前に絞り込む必要がありますneverは決して発生しない型 — 型階層の最下部です。

ts
// any — type checking disabled, use sparingly
function dangerousTransform(input: any) {
  return input.toUpperCase(); // TypeScript won't warn even if this crashes at runtime
}

// unknown — safe alternative, forces you to check before using
function safeTransform(input: unknown): string {
  if (typeof input === 'string') {
    return input.toUpperCase(); // ✅ narrowed to string inside this block
  }
  if (typeof input === 'number') {
    return input.toFixed(2);    // ✅ narrowed to number
  }
  throw new Error(`Cannot transform value of type ${typeof input}`);
}

// Common use: parsing untrusted data (API responses, localStorage, user input)
function parseConfig(raw: unknown): ResolvedConfig {
  if (
    typeof raw === 'object' &&
    raw !== null &&
    'apiUrl' in raw &&
    typeof (raw as any).apiUrl === 'string'
  ) {
    return raw as ResolvedConfig;
  }
  throw new Error('Invalid config format');
}

// never — the exhaustive switch pattern
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.radius ** 2;
    case 'square': return shape.side ** 2;
    default:
      // If you add a new Shape variant and forget to handle it here,
      // TypeScript will error: Type 'NewShape' is not assignable to type 'never'
      const _exhaustive: never = shape;
      throw new Error(`Unhandled shape: ${_exhaustive}`);
  }
}

実際の型の絞り込み

型の絞り込みは、TypeScriptが広い型(string | numberunknown、判別共用体)を条件ブロック内の特定の型に絞り込む方法です。 TypeScriptの絞り込みドキュメントは各ガードを詳しく説明しています — 毎日書くパターンを示します。

ts
// typeof — primitive narrowing
function formatValue(val: string | number | boolean): string {
  if (typeof val === 'string')  return val.trim();
  if (typeof val === 'number')  return val.toLocaleString();
  return val ? 'Yes' : 'No';
  // TypeScript knows val must be boolean here
}

// instanceof — class/object narrowing
function handleError(err: unknown): string {
  if (err instanceof Error)   return err.message;
  if (typeof err === 'string') return err;
  return 'An unknown error occurred';
}

// in operator — property existence check
type Cat = { meow(): void };
type Dog = { bark(): void };

function makeNoise(animal: Cat | Dog) {
  if ('meow' in animal) {
    animal.meow(); // TypeScript narrows to Cat
  } else {
    animal.bark(); // TypeScript narrows to Dog
  }
}

// Discriminated union narrowing (most common in component state)
type LoadState<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; message: string };

function renderState<T>(state: LoadState<T>) {
  if (state.status === 'success') {
    console.log(state.data);    // ✅ data is accessible
  } else if (state.status === 'error') {
    console.error(state.message); // ✅ message is accessible
  }
}

// Type predicates — custom type guards
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    'email' in obj
  );
}

// After isUser() returns true, TypeScript knows obj is User
function processPayload(payload: unknown) {
  if (isUser(payload)) {
    console.log(payload.email); // ✅ fully typed
  }
}

typeof演算子は最も基本的な絞り込みツールです。instanceofin演算子、判別プロパティチェックと組み合わせることで、anyや安全でないキャストに頼らずに、ほぼあらゆる絞り込みシナリオを処理できます。

まとめ

TypeScriptの型システムは、難解な機能を暗記するよりもファンダメンタルをよく学ぶことを報います。リテラル型と判別共用体はランタイムバグのカテゴリ全体を排除します。ジェネリクスを使えば再利用可能な型安全な抽象化が書けます。PartialPickOmitReturnTypeのようなユーティリティ型は型をDRYに保ちます。そしてunknownと絞り込みにより、データが外部から来るエッジでも型の安全性が得られます。これらすべてを固める最善の方法はインタラクティブに実験することです — TypeScript Playgroundではどんなスニペットでも貼り付けて、セットアップなしで推論される型を即座に確認できます。TypeScriptプロジェクトでJSONデータを扱う場合、このサイトのJSONフォーマッターJSON to TypeScriptツールは実際のAPIペイロードからインターフェース定義を自動的に生成できます。