Vinicius Aguiar
TypeScript

TypeScript をはじめる: JavaScript 開発者のための実践ガイド

2026年4月16日 · 12分で読めます

JavaScript を書いていてまだ TypeScript を使っていないなら、本番環境のバグを防ぐ最も効果的なツールを手放していることになります。TypeScript は別の言語ではありません — 静的型付けを備えた JavaScript です。コンパイラはコードを実行する前にエラーを知らせてくれます。実行した後ではありません。本ガイドでは、実際のプロジェクトで TypeScript を使い始めるために必要なことをすべて紹介します。

なぜ TypeScript なのか

JavaScript は動的です — どの変数も、いつでも何にでもなり得ます。これは柔軟ですが危険です。エラーに気づくのは、コードが実行されてユーザーがそのエラーを目にしたときだけです。

// JavaScript: これはエラーなく実行される
function calculateTotal(price, quantity) {
  return price * quantity
}

calculateTotal('10', 2) // 20 ではなく '102'(文字列)を返す
calculateTotal(10)      // 警告なしで NaN を返す

TypeScript はこうしたエラーを実行に検出します。

// TypeScript: コンパイラが警告する
function calculateTotal(price: number, quantity: number): number {
  return price * quantity
}

calculateTotal('10', 2) // ✗ Erro: Argument of type 'string' is not assignable to 'number'
calculateTotal(10)      // ✗ Erro: Expected 2 arguments, but got 1

基本的な型

TypeScript のプリミティブ型は、JavaScript の型に直接対応しています。

// プリミティブ
let name: string = 'Vinicius'
let age: number = 24
let isActive: boolean = true

// 配列
let tags: string[] = ['react', 'next', 'typescript']
let scores: number[] = [95, 87, 92]

// インラインオブジェクト
let user: { name: string; email: string } = {
  name: 'Vinicius',
  email: 'vini@email.com'
}

実際には、変数を明示的に型付けする必要はほとんどありません — TypeScript が型を自動的に推論します。

// 自動推論 — TS は文字列だとわかっている
let name = 'Vinicius' // 型: string
let age = 24           // 型: number
let tags = ['react']   // 型: string[]

// 必要なときだけ明示的に型付けする
let data: unknown = await fetchFromAPI()

インターフェースと型

より複雑なオブジェクトには interface または type を使います。実用上の違いはごくわずかです — オブジェクトには interface を、ユニオンや合成型には type を使いましょう。

// Interface — オブジェクト用
interface User {
  id: string
  name: string
  email: string
  role: 'admin' | 'user'
  avatar?: string // オプションのフィールド
}

// Type — ユニオンや合成用
type Status = 'pending' | 'confirmed' | 'failed' | 'refunded'

type ApiResponse<T> = {
  data: T
  success: boolean
  error: string | null
}

インターフェースは拡張できます。これは型の継承に便利です。

interface BaseEntity {
  id: string
  createdAt: Date
  updatedAt: Date
}

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

interface Order extends BaseEntity {
  userId: string
  total: number
  status: Status
}

// User は id, createdAt, updatedAt, name, email を持つようになった

型付けされた関数

関数の型付けは、TypeScript が最も真価を発揮する場面です。コンパイラが引数と戻り値を検証します。

// 型付けされたパラメータと戻り値
function createUser(name: string, email: string): User {
  return {
    id: crypto.randomUUID(),
    name,
    email,
    role: 'user',
    createdAt: new Date(),
    updatedAt: new Date()
  }
}

// アロー関数
const formatPrice = (cents: number): string => {
  return `R$ ${(cents / 100).toFixed(2)}`
}

// 非同期関数
async function getUser(id: string): Promise<User | null> {
  const res = await fetch(`/api/users/${id}`)
  if (!res.ok) return null
  return res.json()
}

ジェネリクス: 再利用可能な型

ジェネリクスを使うと、安全性を保ちながら任意の型で動作する関数や型を作成できます。これは TypeScript で最も強力な概念です。

// ジェネリクスなし — 型ごとに関数が必要になる
function getFirstString(arr: string[]): string | undefined {
  return arr[0]
}
function getFirstNumber(arr: number[]): number | undefined {
  return arr[0]
}

// ジェネリクスあり — すべての型に対して1つの関数
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0]
}

getFirst(['a', 'b', 'c']) // 型: string | undefined
getFirst([1, 2, 3])       // 型: number | undefined
getFirst<User>(users)     // 型: User | undefined

ジェネリクスは、API 関数、React のフック、その他あらゆる再利用可能なコードに不可欠です。

// ジェネリックな API レスポンス
async function fetchAPI<T>(endpoint: string): Promise<ApiResponse<T>> {
  const res = await fetch(`/api${endpoint}`)
  const data = await res.json()
  return data as ApiResponse<T>
}

// 使用例 — TS は戻り値の型を知っている
const { data: user } = await fetchAPI<User>('/users/123')
// user は User 型

const { data: orders } = await fetchAPI<Order[]>('/orders')
// orders は Order[] 型

ユーティリティ型: 型を変換する

TypeScript には、既存の型を変換するユーティリティ型が用意されています。私が日常的に使っているものを挙げます。

interface User {
  id: string
  name: string
  email: string
  role: 'admin' | 'user'
}

// Partial — すべてのフィールドがオプションになる
type UpdateUser = Partial<User>
// { id?: string; name?: string; email?: string; role?: ... }

// Pick — 特定のフィールドを選択する
type UserPreview = Pick<User, 'id' | 'name'>
// { id: string; name: string }

// Omit — 特定のフィールドを除外する
type CreateUser = Omit<User, 'id'>
// { name: string; email: string; role: 'admin' | 'user' }

// Record — 型付けされたオブジェクトを作成する
type UserMap = Record<string, User>
// { [key: string]: User }

最も一般的な用途はフォームと API です — そこでは同じ型のバリエーションが必要になります。

// API: ユーザーを作成する(id なし、データベースが生成する)
async function createUser(data: Omit<User, 'id'>): Promise<User> {
  const res = await fetch('/api/users', {
    method: 'POST',
    body: JSON.stringify(data)
  })
  return res.json()
}

// API: 部分的に更新する(すべてのフィールドがオプション)
async function updateUser(id: string, data: Partial<User>): Promise<User> {
  const res = await fetch(`/api/users/${id}`, {
    method: 'PATCH',
    body: JSON.stringify(data)
  })
  return res.json()
}

Enum vs ユニオン型

Enum は TypeScript に存在しますが、実際にはユニオン型のほうがシンプルで、余分な JavaScript コードを生成しません。

// ❌ Enum — 余分な JS コードを生成し、より複雑
enum PaymentStatus {
  PENDING = 'pending',
  CONFIRMED = 'confirmed',
  FAILED = 'failed',
}

// ✅ ユニオン型 — よりシンプルで、オーバーヘッドゼロ
type PaymentStatus = 'pending' | 'confirmed' | 'failed'

// どちらも検証では同じように動作する:
function processPayment(status: PaymentStatus) {
  switch (status) {
    case 'pending':
      return '待機中...'
    case 'confirmed':
      return '確定しました!'
    case 'failed':
      return '失敗しました。'
  }
}

型ガード: 型を絞り込む

値が複数の型を取り得る場合、型ガードは TypeScript がそれぞれの時点でどの型なのかを理解する手助けをします。

type Result = { success: true; data: User } | { success: false; error: string }

function handleResult(result: Result) {
  if (result.success) {
    // ここでは TS は result.data が存在することを知っている
    console.log(result.data.name)
  } else {
    // ここでは TS は result.error が存在することを知っている
    console.error(result.error)
  }
}

// カスタム型ガード
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'name' in value &&
    'email' in value
  )
}

const data: unknown = await fetchSomething()
if (isUser(data)) {
  // ここでは TS は data が User であることを知っている
  console.log(data.email)
}

設定: tsconfig.json

tsconfig.json は、プロジェクトで TypeScript がどのように振る舞うかを定義します。Next.js や Node.js を使うモダンなプロジェクトでは、これが私の推奨するベースです。

{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "incremental": true,
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

最も重要なのは "strict": true です — これがすべての型チェックを有効にします。strict モードのないプロジェクトは、TypeScript の価値のほとんどを失います。

本番で私が使っているパターン

すべてのプロジェクトで TypeScript を使ってきた結果、日々の作業で最もよく適用するパターンは次のとおりです。

  • **any を決して使わない** — 型がわからないときは unknown を使い、型ガードで検証する
  • 推論を優先する — 可能な限り TS に型を推論させ、必要なときだけ明示的に型付けする
  • コンポーネントの props にはインターフェースを — すべての React コンポーネントの prop はインターフェースを持つべき
  • API にはユーティリティ型を — 作成には Omit<User, 'id'>、更新には Partial<User>
  • enum の代わりにユニオン型を — よりシンプルで、ランタイムのオーバーヘッドがない
  • **as const** — 変更すべきでないリテラルの配列やオブジェクトに使う
// as const — 値をリテラル型に変換する
const ROLES = ['admin', 'user', 'moderator'] as const
type Role = typeof ROLES[number] // 'admin' | 'user' | 'moderator'

// React コンポーネントの props
interface ButtonProps {
  label: string
  variant?: 'primary' | 'secondary' | 'ghost'
  onClick: () => void
  disabled?: boolean
}

function Button({ label, variant = 'primary', onClick, disabled }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled} className={variant}>
      {label}
    </button>
  )
}

まとめ

TypeScript は、より多くのコードを書くことではありません — コンパイラが検証できるコードを書くことです。型は意図を文書化し、インターフェースはモジュール間の契約を定義し、ジェネリクスは安全性を失わずに再利用を可能にします。JavaScript から来たなら、学習曲線はなだらかです。まず関数と props に型を付けることから始め、その後ジェネリクスやユーティリティ型へ進みましょう。生産性とコードへの信頼の向上は、すぐに表れます。