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 に型を付けることから始め、その後ジェネリクスやユーティリティ型へ進みましょう。生産性とコードへの信頼の向上は、すぐに表れます。
