TypeScript
目次
- 概要
- 1. TypeScriptとは何か・なぜ使うか
- 2. 環境構築とtsconfig
- 3. 型の基礎:プリミティブと基本構造
- 4. 型推論(Type Inference)
- 5. interfaceとtype alias
- 6. ユニオン型・インターセクション型・リテラル型
- 7. 型の絞り込み(Type Narrowing)
- 8. ジェネリクス(Generics)
- 9. ユーティリティ型
- 10. Mapped TypesとConditional Types
- 11. Template Literal Types
- 12. 関数の型
- 13. クラスと型
- 14. モジュールと宣言ファイル(.d.ts)
- 15. strictモードと主要コンパイラオプション
- 16. 非同期処理の型
- 17. エラーハンドリングパターン
- 18. 実務パターン集
- 19. 型テスト・型ユーティリティ
- 20. よくある落とし穴FAQ
- 21. 演習問題
- 22. 学習ロードマップ(30日)
- 23. 用語集と読み替え表
- 24. エコシステム(Zod, tRPC, ts-pattern, type-fest等)
- 25. TypeScript 5.x新機能
- 26. 実践:型設計のケーススタディ
- 27. パフォーマンスとスケーラビリティ
- 28. デバッグと型エラーの読み方
- 29. TypeScriptとJavaScriptの相互運用
- 付録A: TypeScriptチートシート
- 30. まとめ
- まとめ
- 参考文献
概要
まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。
コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。
TypeScriptは、JavaScriptに静的型付けを加え、大規模なアプリケーション開発を支えるための言語です。
このページでは、型推論、union、generic、mapped type、conditional type、tsconfig、実務設計パターンを整理します。
1. TypeScriptとは何か・なぜ使うか
このセクションでは「TypeScriptがなぜ生まれたのか」「何を解決するのか」「JavaScriptとの本質的な違いは何か」を丁寧に説明します。最初にここをしっかり理解しておくと、型を書く際の「なぜ」が腑に落ちやすくなります。
TypeScriptはMicrosoftが開発した 「JavaScriptの型付きスーパーセット」 です。
TypeScript = JavaScript + 型システム
TypeScriptのコードはコンパイル(正確には「トランスパイル」)されてJavaScriptになります。ランタイムにはJavaScriptが実行されます。
1-1. TypeScriptが生まれた背景
JavaScriptの「大規模開発」という課題
JavaScriptはもともと、Webページに小さなインタラクションを追加するための言語として設計されました。1995年にNetscapeのBrendan Eichがわずか10日間で開発したという逸話は有名です。この言語が、2020年代には数十万行規模のサーバー・クライアント両面のアプリケーションを書くために使われています。
大規模なJavaScriptコードベースが抱える根本的な問題は次の通りです。
- 実行時まで型エラーがわからない: 関数に間違った型の引数を渡しても、実行されるまでエラーが出ない
- リファクタリングが怖い: 関数名を変えたとき、全ての呼び出し箇所を変え忘れてもコンパイラは教えてくれない
- コードの意図が伝わらない:
function process(data)のdataが何を期待しているか、コードを読んだだけではわからない - IDEの補完が弱い: 型情報がないため、エディタがプロパティ名を補完できない
Microsoftが直面した問題
MicrosoftはVisual StudioやOfficeなどの大規模なWebアプリをJavaScriptで開発していました。数百人のエンジニアが数年にわたって開発するコードベースでは、上記の問題が深刻化し、生産性と品質の両面で限界を感じていました。
Anders Hejlsbergという人物
TypeScriptは Anders Hejlsberg(アンダース・ヘルスバーグ) が設計しました。彼はC#やDelphi(Turbo Pascal)の設計者でもあり、静的型付け言語の第一人者です。彼の設計思想は「JavaScriptを置き換えるのではなく、JavaScriptの上に型システムを載せる」というものでした。
この判断によりTypeScriptは:
- 既存のJavaScriptコードをそのまま動かせる(段階的移行が可能)
- 型注釈を省略した箇所は型推論で補う(書くコード量を最小化)
- ランタイムには型情報を残さない(ブラウザはJavaScriptを実行するだけ)
という特徴を持つ言語になりました。2012年に最初のバージョンが公開され、現在では世界中のフロントエンド・バックエンド開発で標準的に使われています。
1-2. 型があることで何が嬉しいか
1. コンパイル時にバグを発見できる
// JavaScript: 実行時にエラー(本番環境でユーザーに見える)
function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json())
}
getUser() // ランタイムエラー: idがundefinedになりURLが壊れる
getUser('abc') // ランタイムエラー: 数値のidを期待するAPIに文字列を渡す
// TypeScript: コンパイル時にエラー(開発中に気づける)
function getUser(id: number): Promise<User> {
return fetch(`/api/users/${id}`).then(r => r.json())
}
getUser() // Error: 引数が足りません
getUser('abc') // Error: stringはnumberに代入できません
2. エディタの補完・ナビゲーション
型情報があるとVSCodeなどのIDEが補完、Hover型情報、定義ジャンプを正確に提供します。
interface User {
id: number
name: string
email: string
createdAt: Date
}
const user: User = await getUser(1)
user. // → id, name, email, createdAtが補完される
user.nme // Error: 'nme' は 'User' に存在しません(typoを即検出)
3. リファクタリングが安全
メソッド名変更・型変更時に影響箇所がすべて型エラーとして表示されます。JavaScriptではgrepで探して手動確認するしかなかった作業が、コンパイラが自動的に教えてくれます。
4. チーム開発でのドキュメント効果
// ❌ JavaScriptの関数シグネチャ: 何を渡せばいいかわからない
function createOrder(userId, items, options) { ... }
// ✅ TypeScriptの関数シグネチャ: 仕様書として読める
function createOrder(
userId: UserId,
items: OrderItem[],
options?: { discount?: number; note?: string }
): Promise<Order> { ... }
型定義が「仕様書」として機能し、コードを読む際の理解を助けます。ドキュメントと違い、コードと型定義は常に一致します(型エラーが出るため)。
1-3. JavaScriptとの関係:スーパーセットとは
「スーパーセット」とは、JavaScriptのコードがそのままTypeScriptとしても有効であることを意味します。
┌─────────────────────────────────┐
│ TypeScript │
│ ┌───────────────────────────┐ │
│ │ JavaScript │ │
│ │ (すべてのJSコードは有効) │ │
│ └───────────────────────────┘ │
│ + 型注釈 │
│ + interface / type │
│ + ジェネリクス │
│ + enum │
│ + アクセス修飾子 (public/private) │
└─────────────────────────────────┘
段階的移行が可能であることが重要です。既存のJavaScriptプロジェクトに .ts ファイルを1つずつ追加しながら移行できます。
TypeScriptコンパイルの流れ
.tsファイル
↓ tsc(TypeScript Compiler)またはBabel/SWC/esbuild
.jsファイル(型注釈は消える)
↓ Node.js / ブラウザが実行
ランタイム動作
重要なポイント: 型はランタイムに存在しない。型はコンパイル時の検査専用です。
1-4. TypeScriptの構造的型付け(Structural Typing)
TypeScriptは**構造的型付け(Structural Typing)を採用しています。これはC#やJavaの名前的型付け(Nominal Typing)**とは根本的に異なります。
// 名前的型付け(JavaやC#の考え方):
// PersonとEmployeeが別の名前なら、たとえ同じ構造でも代入不可
// TypeScript(構造的型付け):
// 構造が一致していれば代入できる
interface Person {
name: string
age: number
}
interface Employee {
name: string
age: number
department: string
}
function greet(person: Person) {
console.log(`こんにちは、${person.name}さん`)
}
const employee: Employee = {
name: 'Alice',
age: 30,
department: 'Engineering'
}
greet(employee) // ✅ EmployeeはPersonの構造を満たしているので代入可能
この設計により、TypeScriptはJavaScriptの「ダックタイピング」の文化と相性が良く、既存のJSライブラリにも自然に型を付けられます。
1-5. TypeScriptが解決しないもの
TypeScriptは万能ではありません。以下はTypeScriptで解決できない問題です。
| 問題 | 理由 |
|---|---|
| ランタイムのデータ検証 | 型はコンパイル時のみ存在。APIから返るJSONの形を型で保証できない |
| パフォーマンス問題 | 型システムはJS実行速度に影響しない |
| ロジックのバグ | 型が正しくてもアルゴリズムが間違っていればバグは起きる |
| ランタイムの型チェック | instanceof 等のJSの機能を使う必要がある |
ランタイムのデータ検証はZodなどのバリデーションライブラリと組み合わせることで解決します(セクション24で詳述)。
1-6. 1-X. このセクションのまとめ
TypeScriptが解く問題:
「大規模JavaScriptの型なし開発における品質・保守性の低下」
設計思想(Anders Hejlsberg):
JSを置き換えず、その上に型システムを載せる
→ 段階的移行・既存JSとの共存
3つの核心:
1. コンパイル時検査 → 実行前にバグを発見
2. 構造的型付け → JS文化と親和性が高い柔軟な型システム
3. 型はランタイムに消える → ブラウザ/Node.jsはJSをそのまま実行
TypeScriptで解決できないこと:
ランタイムのデータ検証(Zodなどと組み合わせる)
ロジックのバグ(テストが必要)
2. 環境構築とtsconfig
このセクションではTypeScriptを使うための環境構築と、プロジェクトの心臓部である tsconfig.json の各設定を深掘りします。設定の意味を理解せずに使うと、型チェックが弱くなったり、ビルドが遅くなる原因になります。
2-1. インストールと初期設定
# インストール
npm install -D typescript
# または
pnpm add -D typescript
# tsconfig.jsonの生成
npx tsc --init
# 単発コンパイル
npx tsc
# 型チェックのみ(ファイル生成なし)
npx tsc --noEmit
# 開発時のwatchモード
npx tsc --watch
実務ではtscを直接実行してコンパイルすることは少なく、型チェックのみ(--noEmit)をCIや エディタが行い、ビルドはVite / esbuild / SWCが担当するパターンが多いです。
2-2. tsconfig.jsonの重要な設定(全解説)
{
"compilerOptions": {
// ターゲットとモジュール
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022", "DOM"],
// 出力設定
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"sourceMap": true,
// strictモード(必ず有効にする)
"strict": true,
// 追加で有効にすると品質が上がる
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
// パス解決
"paths": {
"@/*": ["./src/*"]
},
"baseUrl": "."
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
target — なぜこの設定が必要か
target は出力するJavaScriptのバージョンを指定します。
- ES5 → Internet Explorer対応(現在はほぼ不要)
- ES2017 → async/awaitをネイティブ出力(Node.js 8+)
- ES2022 → top-level await、class fields(現代的なNode.js/ブラウザ)
- ESNext → 最新の構文そのまま出力
target が低いとTypeScriptが自動的にダウンレベルコンパイル(polyfill)しますが、バンドルサイズが増えます。ターゲット環境に合わせて選ぶことが重要です。
module と moduleResolution — なぜこの設定が必要か
module: CommonJS → require() 形式(古いNode.js)
module: NodeNext → package.jsonの"type"フィールドに従う(推奨)
module: ESNext → import/export形式(ブラウザ・バンドラ向け)
module: bundler → バンドラ(Vite等)が解決する前提
moduleResolution: node → 古いNode.jsの解決アルゴリズム
moduleResolution: NodeNext → 拡張子を省略しない厳密な解決
moduleResolution: bundler → バンドラがimportを解決することを前提
典型的な組み合わせ:
| 用途 | module | moduleResolution |
|---|---|---|
| Node.js ESM | NodeNext | NodeNext |
| Next.js / Vite | ESNext | bundler |
| 古いNode.js | CommonJS | node |
strict — なぜこの設定が必要か
“strict”: trueは以下のフラグをまとめて有効にします:
| フラグ | 効果 |
|---|---|
strictNullChecks |
null/undefined を他の型と区別する |
noImplicitAny |
型推論できない引数等に暗黙的 any を禁止 |
strictFunctionTypes |
関数型の引数に反変チェックを適用 |
strictBindCallApply |
bind/call/apply の引数型チェック |
strictPropertyInitialization |
クラスプロパティがconstructorで初期化されることを確認 |
useUnknownInCatchVariables |
catch節の変数を unknown にする(TS4.4+) |
strict: falseで開発することの危険性:
// strict: falseの場合(型チェックが機能しない例)
function getUser(id) { // idは暗黙的にany → エラーにならない
return fetch(`/users/${id}`)
}
let user: User = null // nullが代入できてしまう
user.name // ランタイムエラー: Cannot read properties of null
新規プロジェクトでは必ず “strict”: trueから始めてください。
noUncheckedIndexedAccess — なぜこの設定が必要か
配列やインデックスシグネチャへのアクセスに undefined を含めます。
// noUncheckedIndexedAccess: false(デフォルト)
const arr: string[] = ['a', 'b', 'c']
const first: string = arr[0] // 型はstring(実際は存在しないかも)
const fourth: string = arr[3] // 型はstringだが実際はundefined → バグ
// noUncheckedIndexedAccess: true
const first: string | undefined = arr[0] // undefinedが含まれる
if (first !== undefined) {
first.toUpperCase() // ここではstring確定
}
exactOptionalPropertyTypes — なぜこの設定が必要か
type Config = { timeout?: number }
// exactOptionalPropertyTypes: false
const c: Config = { timeout: undefined } // ✅ 通る(?とundefinedを同一視)
// exactOptionalPropertyTypes: true
const c: Config = { timeout: undefined } // ❌ エラー(?はプロパティが「ない」こと)
const c: Config = {} // ✅(プロパティが存在しない)
const c: Config = { timeout: 1000 } // ✅
これにより「オプショナルプロパティ」と「undefinedを明示的に持つプロパティ」の違いが型レベルで区別されます。
2-3. Next.js + TypeScriptのtsconfig
Next.jsは next dev / next build でバンドルするためtsconfigは型チェック専用に使います。
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
各設定の意味:
noEmit: ファイルを出力しない(Next.jsがビルドを担当するため)isolatedModules: esbuildとの互換性のため、各ファイルを独立してコンパイル可能にする- jsx: preserve: JSXをそのまま出力(Next.jsが変換する)
skipLibCheck:node_modules内の.d.tsの型エラーを無視(ビルド高速化)
2-4. パスエイリアスの設定
大規模プロジェクトでは相対パスのインポートが深くなります。
// ❌ 深い相対パス(読みにくい・移動するたびに変わる)
import { UserService } from '../../../services/user/UserService'
import { formatDate } from '../../utils/date'
// ✅ エイリアス(シンプル・移動しても変わらない)
import { UserService } from '@/services/user/UserService'
import { formatDate } from '@/utils/date'
tsconfig.json での設定:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@utils/*": ["./src/utils/*"]
}
}
}
Viteを使う場合は vite.config.ts にも同じエイリアスを設定する必要があります:
// vite.config.ts
import { defineConfig } from 'vite'
import path from 'path'
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})
2-5. プロジェクト参照(Project References)
モノレポや大規模プロジェクトでは、複数のtsconfigをリンクすることでビルドを高速化できます。
packages/
shared/
tsconfig.json ← 共通型定義
frontend/
tsconfig.json ← sharedを参照
backend/
tsconfig.json ← sharedを参照
// packages/frontend/tsconfig.json
{
"compilerOptions": {
"composite": true, // プロジェクト参照のために必要
"declaration": true // .d.tsを生成(参照元が使う)
},
"references": [
{ "path": "../shared" } // sharedの型定義を参照
]
}
# プロジェクト参照対応ビルド(変更があった部分だけ再ビルド)
npx tsc --build
2-6. 2-X. このセクションのまとめ
tsconfigの基本方針:
"strict": trueを必ず有効化
"noUncheckedIndexedAccess": trueで配列アクセスを安全に
"exactOptionalPropertyTypes": trueでオプショナルを正確に
用途別module/moduleResolution:
Node.js ESM → NodeNext / NodeNext
Next.js/Vite → ESNext / bundler
パスエイリアス:
tsconfigのpathsで定義
バンドラ(Vite等)にも同じ設定が必要
プロジェクト参照(composite):
モノレポで有効。変更部分のみ再コンパイルして高速化
3. 型の基礎:プリミティブと基本構造
このセクションではTypeScriptの型システムの基礎となるプリミティブ型から、any/unknown/never の違い、オブジェクト・配列・タプルの型まで体系的に解説します。「なんとなく型を付けている」状態から「型の意味を理解して使う」状態への橋渡しとなるセクションです。
3-1. プリミティブ型
// JavaScriptの型に対応するTypeScriptの型
const num: number = 42
const str: string = 'hello'
const bool: boolean = true
const nothing: null = null
const undef: undefined = undefined
const big: bigint = 9007199254740993n
const sym: symbol = Symbol('id')
// void: 戻り値が意味を持たない関数
function log(msg: string): void {
console.log(msg)
// return undefinedは許可(return 42は不可)
}
3-2. any / unknown / never の深い比較
この3つはTypeScript初心者が最も混乱しやすい型です。それぞれの「なぜ」を理解することが重要です。
any — 型チェックを完全に無効化する
// ❌ anyは型システムの「緊急脱出口」。使うと型チェックが無効になる
let value: any = 'hello'
value.toUpperCase() // ✅(コンパイルは通る)
value.nonExistent() // ✅(コンパイルは通るが実行時エラーの可能性)
value = 42 // ✅ 何でも代入できる
value = { a: 1 } // ✅
// anyは感染する:anyから取り出した値もanyになる
const user: any = fetchUser()
const name = user.name // nameの型もany
name.trim() // コンパイルは通るがnameが数値だったら実行時エラー
any を使ってよい場面: 段階的移行で一時的に型を付けられない箇所、型定義のない古いJSライブラリとのインターフェース(その場合でも unknown を優先する)。
unknown — 型チェックを強制する安全な any
// ✅ unknownは「型が不明だが安全に扱う」ための型
let value: unknown = fetchSomeData()
// unknownは使う前に型チェックが必須
value.toUpperCase() // ❌ Error: unknownは操作できない
if (typeof value === 'string') {
value.toUpperCase() // ✅ stringと確認してから使う
}
if (value instanceof Error) {
value.message // ✅ Errorと確認してから使う
}
// unknownからanyへの代入はできない(逆方向はOK)
const s: string = value // ❌ Error
const a: any = value // ✅(anyは何でも受け入れる)
unknown を使うべき場面: 外部からのデータ(APIレスポンス、JSON.parseの結果)、catch節のエラー、ジェネリクスの型変数の代替。
never — 到達しえない型
// never: この型の値は存在しない
// 使用場面1: 常にthrowする関数の戻り値
function panic(message: string): never {
throw new Error(message)
// 後続のコードは絶対に実行されない
}
// 使用場面2: 網羅性チェック(詳しくはセクション7)
type Shape = 'circle' | 'square'
function getArea(shape: Shape): number {
if (shape === 'circle') return Math.PI
if (shape === 'square') return 1
// ここに到達する型はnever(すべてのケースを網羅したから)
const _: never = shape // 新しいケースを追加し忘れるとエラーになる
return _
}
// 使用場面3: 条件型でフィルタリング
type NonNullable<T> = T extends null | undefined ? never : T
// nullとundefinedをTから除く
比較表
| 項目 | any |
unknown |
never |
|---|---|---|---|
| 代入できるもの | 何でも | 何でも | nothing(代入不可) |
| 代入できる先 | 何にでも | anyとunknownのみ | 何にでも |
| 操作(メソッド呼び出し等) | 制約なし | 型チェック後のみ | 制約なし(到達しないので) |
| 典型的な用途 | 緊急脱出(避ける) | 外部データ・catch節 | 網羅性チェック・無限ループ |
3-3. リテラル型と const アサーション
// リテラル型: 特定の値だけを表す型
type Status = 'pending' | 'active' | 'inactive'
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6
type BoolLiteral = true // trueだけ(falseは代入不可)
// letとconstの推論の違い
let status = 'active' // 型: string(変更可能だから広い型に推論)
const status2 = 'active' // 型: 'active'(変更不可だからリテラル型に推論)
// as const: オブジェクト・配列のリテラル型化
const config = {
host: 'localhost',
port: 3000
} as const
// 型: { readonly host: 'localhost'; readonly port: 3000 }
// as constがない場合: { host: string; port: number }
const ROLES = ['admin', 'user', 'guest'] as const
// 型: readonly ['admin', 'user', 'guest']
type Role = (typeof ROLES)[number] // 'admin' | 'user' | 'guest'
// as constがない場合: type Role = string(意味がない)
as const を使うべき理由: 定数オブジェクトや列挙値を型として使いたい場合、as const なしでは型が広くなりすぎてユニオン型として機能しません。
3-4. 配列とタプル
// 配列の2つの書き方(どちらも同じ意味)
const nums: number[] = [1, 2, 3]
const strs: Array<string> = ['a', 'b', 'c']
const matrix: number[][] = [[1, 2], [3, 4]]
// タプル: 固定長の配列(各位置の型が決まっている)
const point: [number, number] = [10, 20]
const pair: [string, number] = ['Alice', 30]
point[0] // 型: number
point[2] // Error: 長さ2のタプルにインデックス2は存在しない
// 名前付きタプル(TS4.0+: ドキュメント性が上がる)
const namedPoint: [x: number, y: number] = [10, 20]
// 可変長タプル(rest要素)
type StringThenNumbers = [string, ...number[]]
const a: StringThenNumbers = ['hello', 1, 2, 3]
// Readonly配列(推奨: 配列の変更を防ぐ)
const frozen: ReadonlyArray<number> = [1, 2, 3]
frozen.push(4) // Error: pushはReadonlyArrayにない
3-5. オブジェクト型
// インラインオブジェクト型(1箇所でしか使わない場合)
function greet(user: { name: string; age?: number }): string {
return `Hello, ${user.name}${user.age ? ` (${user.age}歳)` : ''}`
}
// オプショナルプロパティ(?)
type Config = {
host: string
port?: number // あってもなくてもよい
ssl?: boolean
}
// 読み取り専用プロパティ
type ImmutablePoint = {
readonly x: number
readonly y: number
}
const p: ImmutablePoint = { x: 1, y: 2 }
p.x = 3 // Error: 読み取り専用プロパティに代入できません
// インデックスシグネチャ(動的なキー)
type StringMap = {
[key: string]: string
}
const map: StringMap = { a: '1', b: '2' }
// Record型(インデックスシグネチャの代替)
type StatusMessages = Record<string, string>
3-6. enumとconst enum
TypeScriptの enum はJavaScriptにはない型機能です。使う場面と注意点を理解することが重要です。
// 数値enum(デフォルト)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
const dir = Direction.Up // 型: Direction
console.log(Direction.Up) // 0
console.log(Direction[0]) // 'Up'(逆引きができる)
// 文字列enum
enum Status {
Pending = 'PENDING',
Active = 'ACTIVE',
Inactive = 'INACTIVE'
}
const status = Status.Pending // 型: Status
console.log(status) // 'PENDING'
// const enum: コンパイル後にインライン展開される(ランタイムオブジェクトが生成されない)
const enum Color {
Red = 'RED',
Green = 'GREEN',
Blue = 'BLUE'
}
const c = Color.Red
// コンパイル後: const c = 'RED'(オブジェクトへの参照なし)
enumの問題点と代替案
// ❌ enumの問題: 数値enumは型安全でない
enum Permission {
Read = 1,
Write = 2,
Delete = 4
}
function setPermission(p: Permission) {}
setPermission(99) // ❌ 本来はエラーになるべきだが型チェックを通過する
setPermission(Permission.Read) // ✅
// ✅ 代替1: as constを使った定数オブジェクト(型安全)
const Status = {
Pending: 'PENDING',
Active: 'ACTIVE',
Inactive: 'INACTIVE'
} as const
type Status = (typeof Status)[keyof typeof Status]
// 'PENDING' | 'ACTIVE' | 'INACTIVE'
// ✅ 代替2: リテラルユニオム(最もシンプル)
type Direction = 'up' | 'down' | 'left' | 'right'
推奨: 新しいプロジェクトでは enum より as const + リテラルユニオンを使うチームが増えています。enum は既存コードとの互換性のために使う程度にとどめると良いでしょう。
3-7. 型の互換性と代入可能性
TypeScriptの型の「代入可能性(Assignability)」を理解することで、型エラーの原因が見えやすくなります。
// サブタイプは親型に代入可能
// string | numberの「サブタイプ」はstringまたはnumber
const s: string = 'hello'
const sn: string | number = s // ✅ stringはstring | numberのサブタイプ
// 逆は不可
const sn2: string | number = 'hello'
const s2: string = sn2 // ❌ Error: string | numberはstringに代入不可
// オブジェクト型: 「より多くのプロパティを持つ型」はサブタイプ
type Point = { x: number; y: number }
type Point3D = { x: number; y: number; z: number }
const p3d: Point3D = { x: 1, y: 2, z: 3 }
const p: Point = p3d // ✅ Point3DはPointのサブタイプ(余分なプロパティがあってもOK)
// ただしオブジェクトリテラルは余剰プロパティチェックがある
const invalid: Point = { x: 1, y: 2, z: 3 }
// ❌ Error: zはPointにない(変数経由なら通るが直接リテラルは通らない)
3-8. 3-X. このセクションのまとめ
プリミティブ型:
number / string / boolean / null / undefined / bigint / symbol
重要な特殊型:
any → 型チェックを無効化(避ける)
unknown → 安全なany(使う前に型チェックが必要)
never → 到達しえない型(網羅性チェック・無限ループ・throw)
リテラル型とas const:
const x = 'active' as const → 型は 'active'(文字列リテラル型)
オブジェクト/配列にas const → 全プロパティがreadonly + リテラル型
タプルvs配列:
配列 → 可変長、全要素が同じ型
タプル → 固定長、各位置の型が異なってよい
4. 型推論(Type Inference)
このセクションではTypeScriptが自動的に型を決定する「型推論」の仕組みを深く解説します。型推論を理解することで「どこに型注釈を書くべきか」「どこは省略できるか」が判断できるようになります。むやみに型注釈を書くのではなく、推論を活用してコードをすっきりさせるのがTypeScriptらしい書き方です。
4-1. 基本的な型推論
// 基本的な推論(型注釈なしでOK)
const x = 42 // 型: number
const s = 'hello' // 型: string
const arr = [1, 2, 3] // 型: number[]
const obj = { id: 1, name: 'Alice' }
// 型: { id: number; name: string }
// 関数の戻り値も推論される
function add(a: number, b: number) {
return a + b // 戻り値型はnumberと推論
}
// 明示的に書く場合: function add(a: number, b: number): number
// どこに型注釈を書くか(実務的な判断)
// ✅ 引数には必ず書く(推論できないため)
// ✅ 戻り値は複雑な場合や公開APIには書く(ドキュメント効果)
// ✅ 変数は推論が明らかなら省略できる
function processUser(user: User): string {
return user.name.toUpperCase()
}
4-2. 型の拡大(Widening)と収縮(Narrowing)
型の拡大(Widening)
型推論では、変更可能な変数は「より広い型」に推論されます。
// let: 変更可能 → 広い型に推論
let message = 'hello' // 型: string('hello'ではなく)
message = 'world' // ✅ 同じstringなので代入可
// const: 変更不可 → リテラル型に推論
const greeting = 'hello' // 型: 'hello'(stringではなく)
// オブジェクトのlet/constの違い
const user = {
name: 'Alice',
role: 'admin'
}
// 型: { name: string; role: string }(constでもプロパティは変更可能)
// プロパティをリテラル型にするにはas constが必要
const user2 = {
name: 'Alice',
role: 'admin'
} as const
// 型: { readonly name: 'Alice'; readonly role: 'admin' }
型の収縮(Narrowing)
条件分岐の中で型が絞り込まれます(詳しくはセクション7)。
function process(value: string | number) {
// この時点ではstring | number
if (typeof value === 'string') {
value.toUpperCase() // この中ではstringに絞り込まれる
} else {
value.toFixed(2) // この中ではnumberに絞り込まれる
}
}
4-3. 文脈型付け(Contextual Typing)
TypeScriptは「型注釈」がなくても、変数の使われ方から型を推論する「文脈型付け」を行います。
// コールバックの引数型は文脈から推論される
const numbers = [1, 2, 3]
numbers.map(n => n * 2)
// nの型はnumberと推論される(number[] のmapだから)
// イベントハンドラの型も文脈から
document.addEventListener('click', (event) => {
event.clientX // MouseEventのプロパティにアクセスできる
// eventはMouseEventと推論される
})
// オブジェクトリテラルの文脈型付け
type Config = {
onSuccess: (data: User) => void
onError: (error: Error) => void
}
const config: Config = {
onSuccess: (data) => {
// dataはUserと推論される(Configの型定義から)
console.log(data.name)
},
onError: (error) => {
// errorはErrorと推論される
console.error(error.message)
}
}
4-4. typeof と keyof による型の取得
// typeof: 値から型を取得
const user = { id: 1, name: 'Alice', active: true }
type User = typeof user
// 型: { id: number; name: string; active: boolean }
// 関数の型を取得
function getUser(id: number) {
return { id, name: 'Alice' }
}
type GetUserFn = typeof getUser // (id: number) => { id: number; name: string }
// keyof: オブジェクト型のキーのユニオン
type UserKeys = keyof User // 'id' | 'name' | 'active'
// 実用例: 型安全なゲッター
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const name = getProperty(user, 'name') // 型: string
const id = getProperty(user, 'id') // 型: number
getProperty(user, 'age') // Error: 'age'はUserのキーでない
4-5. 型推論の限界と型注釈が必要な場面
TypeScriptの型推論は強力ですが、以下の場合は明示的な型注釈が必要です。
// 1. 関数の引数(推論できない)
function add(a, b) { return a + b } // ❌ a, bがanyに
function add(a: number, b: number) { return a + b } // ✅
// 2. 空の配列(要素の型が推論できない)
const items = [] // 型: never[](何も追加できない)
const items: string[] = [] // ✅
// 3. 初期値がnull/undefined
let user = null // 型: null(Userに広げられない)
let user: User | null = null // ✅
// 4. 複数の型が混在する可能性がある変数
// TypeScriptは最初の代入から型を推論するため
let value = 'hello'
value = 42 // Error: numberはstringに代入できない
let value: string | number = 'hello'
value = 42 // ✅
// 5. 再帰的な型(推論に限界がある)
// 戻り値型を明示しないと推論に失敗することがある
function factorial(n: number): number {
return n <= 1 ? 1 : n * factorial(n - 1)
}
4-6. 型推論のベストプラクティス
型推論は強力ですが、どこに型注釈を書くかの判断が重要です。
型注釈を「書くべき場所」と「省略できる場所」
// ✅ 書くべき場所: 関数の引数(推論できない)
function greet(name: string): string {
return `こんにちは、${name}さん`
}
// ✅ 書くべき場所: 公開APIの戻り値(ドキュメント・意図の明示)
export function createUser(input: CreateUserInput): Promise<User> {
// 戻り値型を明示することでAPIの契約が明確になる
return db.user.create({ data: input })
}
// ✅ 書くべき場所: 変数の型が曖昧なとき
let currentUser: User | null = null // nullで初期化するので型注釈が必要
// ✅ 書くべき場所: 空の配列・オブジェクト
const items: string[] = []
const cache: Map<string, User> = new Map()
// ⭕ 省略できる場所: 代入から型が明らかなとき
const name = 'Alice' // stringと明らか
const count = 0 // numberと明らか
const isActive = true // booleanと明らか
// ⭕ 省略できる場所: 関数の戻り値が単純なとき(内部実装)
function add(a: number, b: number) {
return a + b // numberと推論される(公開APIでなければOK)
}
// ⭕ 省略できる場所: コールバックの引数
const numbers = [1, 2, 3]
const doubled = numbers.map(n => n * 2) // nはnumberと推論
型注釈の粒度を考える
// ❌ 過剰な型注釈(推論に任せてよい場所)
const x: number = 42 // 42からnumberと明らか
const arr: number[] = [1, 2, 3].map((n: number): number => n * 2)
// ✅ 適切な型注釈
const x = 42
const arr = [1, 2, 3].map(n => n * 2)
// ❌ 不足な型注釈(anyになってしまう)
function process(data) { // dataがanyに
return data.name
}
// ✅ 適切な型注釈
function process(data: UserLike): string {
return data.name
}
// 実務ルール:
// - 引数: 必ず型を書く
// - 戻り値: 複雑または公開APIは書く、単純な内部関数は推論に任せる
// - 変数: 型が自明なら省略、不明瞭なら書く
4-7. 4-X. このセクションのまとめ
型推論の原則:
let → 広い型(string, numberなど)
const → リテラル型('hello', 42など)
const + as const → オブジェクト/配列もリテラル型に
型注釈が必要な場面:
関数の引数(必須)
空配列の初期化
null/undefinedで初期化する変数
公開APIの戻り値型(ドキュメント効果)
文脈型付け:
コールバック引数は親の型定義から自動推論される
イベントハンドラのevent型はリスナー名から推論される
keyof / typeof:
typeof value → 値から型を取得
keyof Type → オブジェクト型のキーのユニオンを取得
5. interfaceとtype alias
このセクションではTypeScriptで型を定義する2つの主要な手段、interface と type alias の違い・使い分けを徹底解説します。「どちらを使えばいいか」はTypeScript開発者がよく悩む問題です。それぞれの強みと弱みを理解し、プロジェクトで一貫した判断ができるようにします。
5-1. interface(インターフェイス)
interface User {
id: number
name: string
email?: string // オプショナル
readonly createdAt: Date // 読み取り専用
}
// インターフェイスの拡張(継承)
interface AdminUser extends User {
role: 'admin'
permissions: string[]
}
// 複数のインターフェイスを継承
interface Auditable {
createdBy: string
updatedBy: string
}
interface SuperAdmin extends AdminUser, Auditable {
superPower: string
}
5-2. type alias(型エイリアス)
// プリミティブ、ユニオン、タプルなど幅広く使える
type ID = string | number
type Point = [number, number]
type Status = 'pending' | 'active' | 'inactive'
// オブジェクト型(interfaceと同様に使える)
type User = {
id: number
name: string
}
// 型エイリアスの組み合わせ
type CreateUserInput = Omit<User, 'id' | 'createdAt'>
type UserWithTimestamps = User & {
createdAt: Date
updatedAt: Date
}
5-3. interface vs type alias — 技術的な違い
| 機能 | interface | type alias |
|---|---|---|
| オブジェクト型 | ✅ | ✅ |
| プリミティブ型 | ❌ | ✅ |
| ユニオン型 | ❌ | ✅ |
| タプル型 | ❌(配列なら可) | ✅ |
| 交差型(&) | extendsで代替 | ✅ |
| 宣言マージ | ✅ | ❌ |
implements で使える |
✅ | ✅(オブジェクト型のみ) |
| Computed Property Names | ❌ | ✅ |
| 再帰的な型 | ✅(自己参照OK) | △(一部制限あり) |
| エラーメッセージの読みやすさ | ✅(名前が出る) | △(展開されることも) |
5-4. 宣言マージ(Declaration Merging)の深掘り
interface は同じ名前で複数回宣言することで型を**マージ(合成)**できます。これは type alias にはない機能です。
// 宣言マージの基本
interface User {
id: number
name: string
}
interface User {
email: string // 後から追加
}
// 合成された結果
const user: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com' // 必須(マージされているため)
}
宣言マージの実用例: グローバル型への追加
// ExpressのRequest型に独自プロパティを追加
declare module 'express' {
interface Request {
user?: {
id: string
role: string
}
}
}
// Windowオブジェクトに独自プロパティを追加
interface Window {
analytics: {
track(event: string, properties?: Record<string, unknown>): void
}
}
// 使う側
window.analytics.track('page_view', { path: '/home' })
宣言マージの注意点: ライブラリの型定義が宣言マージで拡張可能であることは便利な反面、意図しない拡張が入ることもあります。type alias を使えばこのリスクはありません。
5-5. 使い分けの意思決定ガイド
新しい型を定義するとき、以下の順で判断する:
1. ユニオン型が必要か?
→ typeを使う(interfaceではユニオンを直接定義できない)
例: type Status = 'pending' | 'active'
2. プリミティブ型の別名か?
→ typeを使う
例: type UserId = string
3. タプル型か?
→ typeを使う
例: type Point = [number, number]
4. Mapped Types / Conditional Typesを使うか?
→ typeを使う(interfaceでは使えない)
例: type Partial<T> = { [K in keyof T]?: T[K] }
5. 宣言マージが必要か?(ライブラリ型の拡張など)
→ interfaceを使う
6. クラスのimplementsで使うか? (設計上の明示性)
→ interfaceを使う(慣習的)
7. 上記以外のオブジェクト型:
→ チームで統一する(どちらでもよい)
5-6. ❌ と ✅ の実例比較
// ❌ interfaceでユニオン型を定義しようとする(できない)
interface Status {
// ユニオンは書けない
}
// ✅ typeを使う
type Status = 'pending' | 'active' | 'inactive'
// ❌ typeで宣言マージしようとする(できない)
type Config = { host: string }
type Config = { port: number } // Error: 識別子 'Config' が重複しています
// ✅ interfaceを使う
interface Config { host: string }
interface Config { port: number } // ✅ マージされる
// ❌ 再利用する型をインラインで書く(再利用できない・読みにくい)
function createUser(input: { name: string; email: string; age?: number }): { id: number; name: string; email: string; age?: number; createdAt: Date } { ... }
// ✅ type/interfaceで名前を付ける
interface CreateUserInput {
name: string
email: string
age?: number
}
interface User extends CreateUserInput {
id: number
createdAt: Date
}
function createUser(input: CreateUserInput): User { ... }
5-7. 5-X. このセクションのまとめ
interfaceの強み:
宣言マージ(ライブラリ型の拡張)
extendsによる継承
エラーメッセージが読みやすい
typeの強み:
ユニオン型・タプル型・プリミティブ型の別名
Mapped Types / Conditional Types
交差型(&)
意思決定フロー:
ユニオン/タプル/プリミティブ → type
宣言マージが必要 → interface
それ以外のオブジェクト型 → チームで統一
実務的な判断:
新規プロジェクトはtypeに統一しているチームが多い
ライブラリ作者はinterface(拡張性を保つため)
チームで混在させないことが最も重要
6. ユニオン型・インターセクション型・リテラル型
このセクションではTypeScriptの型システムの中核をなす「型の組み合わせ」を解説します。ユニオン型・インターセクション型・リテラル型を使いこなすことで、実際のビジネスロジックを正確に型で表現できるようになります。特に「判別可能なユニオン(Discriminated Union)」パターンは実務で非常によく使います。
6-1. ユニオン型(どちらか)
// どちらかの型(| で結合)
type StringOrNumber = string | number
function format(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase() // ここではstring確定
}
return value.toFixed(2) // ここではnumber確定
}
// nullableな型(strictモードではnull/undefinedは明示が必要)
type NullableString = string | null
type MaybeUser = User | undefined
// 使い方
function findUser(id: number): User | null {
const user = db.find(id)
return user ?? null
}
const user = findUser(1)
user.name // ❌ Error: userがnullかもしれない
user?.name // ✅ Optional chaining
if (user !== null) {
user.name // ✅ nullを除外してから使う
}
6-2. 判別可能なユニオン(Discriminated Union)パターン
判別可能なユニオンはTypeScriptで最もよく使われるパターンの一つです。各バリアントに 共通のリテラル型プロパティ(判別子) を持たせることで、型の絞り込みと網羅性チェックが可能になります。
// 判別子: kindプロパティ(共通のリテラル型)
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'rectangle'; width: number; height: number }
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2 // radiusのみアクセス可
case 'square':
return shape.side ** 2 // sideのみアクセス可
case 'rectangle':
return shape.width * shape.height // width, heightのみアクセス可
}
}
switchの網羅性チェック(Exhaustiveness Checking)
新しいバリアントを追加したとき、処理を書き忘れるとコンパイルエラーになるようにする重要なパターンです。
// ❌ 網羅性チェックなし: 新しいケースを追加しても気づかない
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
// triangleを忘れた! → 実行時にundefinedが返る
}
return 0 // これがあるとエラーにならない
}
// ✅ 網羅性チェックあり: triangleを追加したらコンパイルエラーになる
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: {
// すべてのケースを網羅していればshapeはnever型
const _exhaustive: never = shape
throw new Error(`未対応のShape: ${JSON.stringify(_exhaustive)}`)
}
}
}
// triangleを追加後area() を更新し忘れると:
// Error: Type '{ kind: "triangle"; ... }' is not assignable to type 'never'
実務的な判別可能なユニオンの例
// APIのステータス管理
type ApiState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
function renderState<T>(state: ApiState<T>): string {
switch (state.status) {
case 'idle': return '開始前'
case 'loading': return 'ロード中...'
case 'success': return `データ: ${JSON.stringify(state.data)}`
case 'error': return `エラー: ${state.error.message}`
default: {
const _: never = state
throw new Error('未対応のステータス')
}
}
}
// イベントシステム
type AppEvent =
| { type: 'USER_LOGIN'; userId: string; timestamp: Date }
| { type: 'USER_LOGOUT'; userId: string }
| { type: 'ITEM_PURCHASED'; itemId: string; price: number }
function handleEvent(event: AppEvent) {
switch (event.type) {
case 'USER_LOGIN':
console.log(`ログイン: ${event.userId}`)
break
case 'USER_LOGOUT':
console.log(`ログアウト: ${event.userId}`)
break
case 'ITEM_PURCHASED':
console.log(`購入: ${event.itemId} ¥${event.price}`)
break
}
}
6-3. インターセクション型(全てを持つ)
type Named = { name: string }
type Aged = { age: number }
// 両方のプロパティを持つ
type Person = Named & Aged
const person: Person = { name: 'Alice', age: 30 }
// 用途1: Mixinパターン
type WithTimestamps<T> = T & {
createdAt: Date
updatedAt: Date
}
type UserRecord = WithTimestamps<User>
// { id, name, email, createdAt, updatedAt }
// 用途2: typeの合成
type ApiClient = HttpClient & CacheClient & LoggingClient
// 注意: 矛盾した型の交差はneverになる
type Impossible = string & number // never(stringかつnumberは存在しない)
6-4. インターセクションvs継承の違い
// interface extends(継承)との違い
interface A { x: string }
interface B extends A { y: number }
// BはAを継承 → xはstring確定
// type intersection
type C = { x: string }
type D = { x: number }
type E = C & D
// E.xはstring & number → never(矛盾する型の交差)
// ❌ これは意図通りにならない
const e: E = { x: 'hello' } // Error: stringはneverに代入できない
6-5. 6-X. このセクションのまとめ
ユニオン型(|):
「AまたはB」を表す
型ガードで絞り込んで使う
nullableの表現に使う(string | null)
判別可能なユニオン:
共通のリテラル型プロパティ(判別子)を持たせる
switch/ifで絞り込む
default: neverで網羅性チェック → 新ケース追加時の安全網
インターセクション型(&):
「AかつB」を表す(両方のプロパティを持つ)
Mixinパターンや型の合成に使う
矛盾する型の交差はneverになる注意点あり
7. 型の絞り込み(Type Narrowing)
このセクションでは「広い型から具体的な型に絞り込む」Narrowingの全技法を解説します。TypeScriptがコードの流れ(制御フロー)を読んで自動的に型を確定する仕組みを理解することで、型アサーション(as)に頼らず安全にコードを書けるようになります。
7-1. typeofによる絞り込み
function process(value: string | number | boolean) {
if (typeof value === 'string') {
return value.toUpperCase() // ここではstring
}
if (typeof value === 'number') {
return value.toFixed(2) // ここではnumber
}
return value ? 'yes' : 'no' // ここではboolean
}
7-2. instanceofによる絞り込み
function handleError(error: unknown) {
if (error instanceof Error) {
console.error(error.message)
console.error(error.stack)
} else if (typeof error === 'string') {
console.error(error)
} else {
console.error('Unknown error:', error)
}
}
// カスタムエラークラスでの使用
class NetworkError extends Error {
constructor(public readonly statusCode: number, message: string) {
super(message)
this.name = 'NetworkError'
}
}
class ValidationError extends Error {
constructor(public readonly field: string, message: string) {
super(message)
this.name = 'ValidationError'
}
}
function handleAppError(error: NetworkError | ValidationError) {
if (error instanceof NetworkError) {
retry(error.statusCode) // statusCodeにアクセス可
} else {
showFieldError(error.field) // fieldにアクセス可
}
}
7-3. in演算子による絞り込み
type Cat = { meow(): void }
type Dog = { bark(): void }
function makeSound(animal: Cat | Dog) {
if ('meow' in animal) {
animal.meow() // ここではCat
} else {
animal.bark() // ここではDog
}
}
// 判別子のないユニオン型に有効
type ApiSuccess = { data: unknown; requestId: string }
type ApiError = { error: string; code: number }
type ApiResponse = ApiSuccess | ApiError
function processResponse(response: ApiResponse) {
if ('data' in response) {
processData(response.data) // ApiSuccess
} else {
handleError(response.error) // ApiError
}
}
7-4. 型ガード関数(User-Defined Type Guards)
// 型ガード関数: 戻り値が `value is Type`
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
typeof (value as Record<string, unknown>).id === 'number' &&
'name' in value &&
typeof (value as Record<string, unknown>).name === 'string'
)
}
function process(data: unknown) {
if (isUser(data)) {
console.log(data.name) // ここではUser型が保証される
}
}
// 配列のフィルタリングに型ガードを使う
const maybeUsers: (User | null)[] = [user1, null, user2, null]
// ❌ 普通のfilterでは型が残る
const users1 = maybeUsers.filter(u => u !== null)
// 型: (User | null)[](nullが除かれていない)
// ✅ 型ガードを使うと型が絞り込まれる
function isNotNull<T>(value: T | null): value is T {
return value !== null
}
const users2 = maybeUsers.filter(isNotNull)
// 型: User[](正しく絞り込まれる)
7-5. アサーション関数(Assertion Functions)
TypeScript 3.7+ で追加された asserts キーワードを使うと、条件を確認した後の型を変えられます。
// asserts val is string: この関数が返ればvalはstring確定
function assertIsString(val: unknown): asserts val is string {
if (typeof val !== 'string') {
throw new TypeError(`Expected string, got ${typeof val}`)
}
}
function process(val: unknown) {
assertIsString(val) // throwしなければ以降はstring型
val.toUpperCase() // ✅ string確定
}
// nullチェックのアサーション
function assertDefined<T>(val: T | null | undefined, name: string): asserts val is T {
if (val == null) {
throw new Error(`${name} は必須です`)
}
}
const user = findUser(id) // User | null
assertDefined(user, 'user')
user.name // ✅ nullが除外されている
7-6. 網羅性チェックと never による安全網
// exhaustiveCheckヘルパー関数
function exhaustiveCheck(value: never, message?: string): never {
throw new Error(message ?? `網羅されていないケース: ${JSON.stringify(value)}`)
}
// 使用例
type Color = 'red' | 'green' | 'blue'
function getHex(color: Color): string {
switch (color) {
case 'red': return '#FF0000'
case 'green': return '#00FF00'
case 'blue': return '#0000FF'
default:
return exhaustiveCheck(color)
// Colorに 'yellow' を追加したとき、ここでコンパイルエラーになる
}
}
// ApiResultの網羅チェック
type ApiResult<T> =
| { status: 'success'; data: T }
| { status: 'error'; message: string }
| { status: 'loading' }
function render<T>(result: ApiResult<T>) {
switch (result.status) {
case 'success':
return renderData(result.data)
case 'error':
return renderError(result.message)
case 'loading':
return renderSpinner()
default:
return exhaustiveCheck(result)
}
}
7-7. 制御フロー解析(Control Flow Analysis)
TypeScriptはif/else、return、throwなどの制御フローを解析して型を絞り込みます。
function processInput(input: string | null | undefined): string {
// パターン1: 早期リターン(null/undefinedを除外)
if (input == null) {
return 'デフォルト値'
}
// この行以降、inputはstring確定
return input.toUpperCase()
}
// タグ付きユニオンの制御フロー
function processState(state: ApiState<User>) {
if (state.status !== 'success') {
return null
}
// ここではstatus === 'success' 確定
return state.data.name // dataにアクセス可
}
7-8. 7-X. このセクションのまとめ
Narrowingの手法:
typeof → プリミティブ型の絞り込み
instanceof → クラスインスタンスの絞り込み
in → プロパティの存在チェック
型ガード関数 → value is Typeを返す関数
アサーション関数 → asserts val is Type
網羅性チェック:
switchのdefaultにneverを代入
exhaustiveCheck(value: never) ヘルパーを作る
新しいケース追加時にコンパイルエラーで気づける
制御フロー解析:
TypeScriptはif/return/throwを追跡して型を絞り込む
早期リターンパターンでネストを減らしながら型も絞れる
8. ジェネリクス(Generics)
このセクションではTypeScriptのジェネリクスを基礎から応用まで解説します。ジェネリクスは「型の引数」であり、コードを再利用しながら型安全を維持するための仕組みです。any を使わずに汎用的なコードを書くために不可欠な機能です。
8-1. ジェネリクスの基本
// Tは型変数(Type Variable)
function identity<T>(value: T): T {
return value
}
identity(42) // Tはnumberに推論される
identity('hello') // Tはstringに推論される
identity<boolean>(true) // 明示的に指定
// 複数の型変数
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b]
}
pair(1, 'hello') // [number, string]
8-2. 型変数の制約(extends)
// Tはstringかnumberでなければならない
function format<T extends string | number>(value: T): string {
return String(value)
}
// Tはlengthプロパティを持つ型でなければならない
function getLength<T extends { length: number }>(value: T): number {
return value.length
}
getLength('hello') // ✅ string.length
getLength([1, 2, 3]) // ✅ Array.length
getLength({ length: 5 }) // ✅ カスタムオブジェクト
getLength(42) // ❌ Error: numberにlengthがない
// keyofと組み合わせる
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
// デフォルト型パラメータ(TS2.3+)
interface Repository<T, ID = number> {
findById(id: ID): Promise<T | null>
findAll(): Promise<T[]>
save(entity: T): Promise<T>
delete(id: ID): Promise<void>
}
8-3. ジェネリクスの実用例
// APIレスポンスの汎用ラッパー
type ApiResponse<T> = {
data: T
meta: {
total: number
page: number
perPage: number
}
requestId: string
}
type UsersResponse = ApiResponse<User[]>
type UserResponse = ApiResponse<User>
// ジェネリックなキャッシュ
class Cache<T> {
private store = new Map<string, { value: T; expiresAt: number }>()
set(key: string, value: T, ttlMs = 60_000): void {
this.store.set(key, {
value,
expiresAt: Date.now() + ttlMs
})
}
get(key: string): T | undefined {
const entry = this.store.get(key)
if (!entry) return undefined
if (Date.now() > entry.expiresAt) {
this.store.delete(key)
return undefined
}
return entry.value
}
}
const userCache = new Cache<User>()
userCache.set('user:1', { id: 1, name: 'Alice' })
const user = userCache.get('user:1') // 型: User | undefined
8-4. 型安全なAPIクライアントの実例
// ジェネリクスを使った型安全なfetchラッパー
async function apiFetch<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const response = await fetch(`/api${endpoint}`, {
headers: { 'Content-Type': 'application/json' },
...options
})
if (!response.ok) {
throw new Error(`API Error: ${response.status}`)
}
return response.json() as Promise<T>
}
// 使う側は型引数で戻り値型を指定
const user = await apiFetch<User>('/users/1') // 型: User
const users = await apiFetch<User[]>('/users') // 型: User[]
// エンドポイントマップで型をさらに安全に
type ApiEndpoints = {
'/users': User[]
'/users/:id': User
'/posts': Post[]
}
class ApiClient {
async get<Path extends keyof ApiEndpoints>(
path: Path
): Promise<ApiEndpoints[Path]> {
const response = await fetch(`/api${path}`)
return response.json()
}
}
const client = new ApiClient()
const users = await client.get('/users') // 型: User[]
const user2 = await client.get('/users/:id') // 型: User
8-5. infer キーワード
infer はConditional Typesの中で、型の一部を「推論して取り出す」キーワードです。
// 配列の要素型を取り出す
type ArrayElement<T> = T extends (infer Item)[] ? Item : never
type StrElement = ArrayElement<string[]> // string
// Promiseの解決型を取り出す
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T
type A = Awaited<Promise<Promise<string>>> // string
// 関数の戻り値型(ReturnTypeの実装)
type MyReturnType<T> = T extends (...args: never[]) => infer R ? R : never
type R = MyReturnType<() => string> // string
// 関数の第1引数の型
type FirstParam<T> = T extends (first: infer F, ...rest: never[]) => unknown ? F : never
type F = FirstParam<(id: number, name: string) => void> // number
// タプルの先頭要素
type Head<T extends readonly unknown[]> =
T extends readonly [infer H, ...unknown[]] ? H : never
type H = Head<[string, number, boolean]> // string
8-6. 共変・反変(Variance)の基礎
TypeScriptのジェネリクスには「型の方向性」があります。
// 共変(Covariance): より具体的な型を広い型として使える
// 戻り値の型は共変
type Producer<T> = () => T
const strProducer: Producer<string> = () => 'hello'
const anyProducer: Producer<string | number> = strProducer // ✅
// stringを返す関数はstring | numberを返す関数として使える
// 反変(Contravariance): より広い型を具体的な型として使える
// 引数の型は反変(strictFunctionTypes: trueのとき)
type Consumer<T> = (value: T) => void
const anyConsumer: Consumer<string | number> = (v) => console.log(v)
const strConsumer: Consumer<string> = anyConsumer // ✅
// string | numberを受け取る関数はstringを受け取る関数として使える
// ❌ 逆方向はエラー
const strOnly: Consumer<string> = (v: string) => v.toUpperCase()
const anyConsumer2: Consumer<string | number> = strOnly // ❌
// stringを受け取る関数にnumberを渡される可能性があるため
8-7. 分散条件型(Distributive Conditional Types)
// Tがユニオン型のとき、各メンバーに分散する
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number>
// string[] | number[](分散適用)
// 分散を防ぐ([T] でラップする)
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type Result2 = ToArrayNonDist<string | number>
// (string | number)[]
8-8. ジェネリクスの高度なパターン
型レベルの条件分岐とジェネリクスの組み合わせ
// TがPromiseなら解決型、そうでなければTのまま
type Resolved<T> = T extends Promise<infer U> ? U : T
type A = Resolved<Promise<string>> // string
type B = Resolved<number> // number
// 型安全な非同期ラッパー
async function resolveAll<T extends Record<string, unknown>>(
obj: T
): Promise<{ [K in keyof T]: Resolved<T[K]> }> {
const result = {} as { [K in keyof T]: Resolved<T[K]> }
for (const key of Object.keys(obj) as (keyof T)[]) {
const value = obj[key]
result[key] = (
value instanceof Promise ? await value : value
) as Resolved<T[typeof key]>
}
return result
}
const data = await resolveAll({
user: fetchUser(1), // Promise<User>
config: fetchConfig(), // Promise<Config>
timestamp: Date.now() // number(Promiseでない)
})
// 型: { user: User; config: Config; timestamp: number }
ジェネリクスを使った型安全なEvent Bus
// 型安全なイベントバスのジェネリック実装
type EventHandler<T> = (event: T) => void | Promise<void>
class EventBus<EventMap extends Record<string, unknown>> {
private handlers = new Map<
keyof EventMap,
Set<EventHandler<EventMap[keyof EventMap]>>
>()
subscribe<K extends keyof EventMap>(
event: K,
handler: EventHandler<EventMap[K]>
): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set())
}
const set = this.handlers.get(event)!
set.add(handler as EventHandler<EventMap[keyof EventMap]>)
// 購読解除関数を返す(クリーンアップに使う)
return () => {
set.delete(handler as EventHandler<EventMap[keyof EventMap]>)
}
}
async publish<K extends keyof EventMap>(
event: K,
data: EventMap[K]
): Promise<void> {
const handlers = this.handlers.get(event)
if (!handlers) return
await Promise.all([...handlers].map(h => h(data)))
}
}
// 使用例
type OrderEvents = {
'order.created': { orderId: string; total: number }
'order.paid': { orderId: string; paymentId: string }
'order.shipped': { orderId: string; trackingNumber: string }
}
const orderBus = new EventBus<OrderEvents>()
const unsubscribe = orderBus.subscribe('order.created', async ({ orderId, total }) => {
await sendOrderConfirmation(orderId, total)
})
await orderBus.publish('order.created', {
orderId: 'ord_123',
total: 9800
})
// 型チェック: totalはnumberが必要
unsubscribe() // クリーンアップ
Phantom Types(幽霊型)パターン
型パラメータが実行時の値に影響しないが、型レベルでの区別に使われるパターンです。
// 「センチメートル」と「メートル」を型レベルで区別
type Centimeters = { readonly __unit: 'cm' }
type Meters = { readonly __unit: 'm' }
type Length<Unit> = number & { readonly __phantom: Unit }
type CM = Length<Centimeters>
type M = Length<Meters>
function cm(value: number): CM { return value as CM }
function m(value: number): M { return value as M }
function cmToM(value: CM): M { return (value / 100) as M }
const height = cm(170) // CM型
const distance = m(5) // M型
const total = height + distance // ❌(CMとMを足そうとしている)
const heightInM = cmToM(height) // ✅(変換してから使う)
const total2 = heightInM + distance // ✅
8-9. 8-X. このセクションのまとめ
ジェネリクスの目的:
anyを使わずに汎用的なコードを書く
型パラメータTを関数/クラス/型に持たせる
制約(extends):
<T extends string | number> → 特定の型のみ許可
<T extends { length: number }> → 特定の構造を要求
<K extends keyof T> → Tのキーのみ許可
inferキーワード:
条件型の中で型の一部を「取り出す」
ArrayElement, Awaited, ReturnTypeなどの実装に使う
分散条件型:
ユニオン型に条件型を適用すると各メンバーに分散する
[T] extends [any] でラップすると分散を防げる
共変・反変:
戻り値は共変(より具体的な型で代替可能)
引数は反変(より広い型で代替可能)
9. ユーティリティ型
このセクションではTypeScript組み込みのユーティリティ型を網羅的に解説し、さらに自分でユーティリティ型を実装する方法を学びます。組み込み型を使いこなすことで、型変換のためのコードを大幅に削減できます。
9-1. よく使う組み込みユーティリティ型
type User = {
id: number
name: string
email: string
password: string
createdAt: Date
}
// Partial<T>: すべてのプロパティをオプショナルに
type UserUpdateInput = Partial<User>
// { id?: number; name?: string; ... }
// Required<T>: すべてのオプショナルを必須に
type Config = { host?: string; port?: number }
type RequiredConfig = Required<Config>
// { host: string; port: number }
// Readonly<T>: すべてを読み取り専用に
type ImmutableUser = Readonly<User>
// Pick<T, K>: 特定のプロパティだけを取り出す
type UserPublic = Pick<User, 'id' | 'name' | 'email'>
// Omit<T, K>: 特定のプロパティを除く
type UserWithoutPassword = Omit<User, 'password'>
// Record<K, V>: キーと値の型からオブジェクト型を作る
type UserById = Record<number, User>
type StatusMessages = Record<'pending' | 'active' | 'inactive', string>
// Exclude<T, U>: TからUに代入可能な型を除く
type StringOnly = Exclude<string | number | boolean, number | boolean>
// string
// Extract<T, U>: TからUに代入可能な型だけ取り出す
type NumberOrString = Extract<string | number | boolean, string | number>
// string | number
// NonNullable<T>: nullとundefinedを除く
type DefinitelyString = NonNullable<string | null | undefined>
// string
// ReturnType<T>: 関数の戻り値型
function getUser() { return { id: 1, name: 'Alice' } }
type UserShape = ReturnType<typeof getUser>
// { id: number; name: string }
// Parameters<T>: 関数の引数型をタプルで
function createUser(name: string, age: number, role: string) {}
type CreateUserParams = Parameters<typeof createUser>
// [string, number, string]
// Awaited<T>: Promiseの解決型(TS4.5+)
type ResolvedUser = Awaited<Promise<User>> // User
type NestedResolved = Awaited<Promise<Promise<string>>> // string
// ConstructorParameters<T>: コンストラクタの引数型
class UserService {
constructor(private repo: UserRepository, private logger: Logger) {}
}
type ServiceParams = ConstructorParameters<typeof UserService>
// [UserRepository, Logger]
// InstanceType<T>: クラスのインスタンス型
type ServiceInstance = InstanceType<typeof UserService>
// UserService
9-2. 組み合わせて使う実務パターン
// CRUDのInput型を導出
type BaseEntity = {
id: number
createdAt: Date
updatedAt: Date
}
type Post = BaseEntity & {
title: string
content: string
authorId: number
published: boolean
}
// 作成時: idとタイムスタンプは不要
type CreatePostInput = Omit<Post, keyof BaseEntity>
// 更新時: すべてオプショナル、idは必須
type UpdatePostInput = Partial<Omit<Post, keyof BaseEntity>> & { id: number }
// APIレスポンス: 機密情報を除く
type PostResponse = Omit<Post, 'authorId'> & { author: { name: string } }
9-3. 自分でユーティリティ型を作る
DeepPartial — ネストしたオブジェクトも再帰的にオプショナルにする
// 組み込みのPartial<T> は1階層のみオプショナルにする
// DeepPartial: 再帰的にオプショナル化
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? T[K] extends (...args: never[]) => unknown
? T[K] // 関数はそのまま
: DeepPartial<T[K]>
: T[K]
}
type Config = {
server: {
host: string
port: number
ssl: { enabled: boolean; cert: string }
}
database: { url: string; poolSize: number }
}
type PartialConfig = DeepPartial<Config>
// server?: { host?: string; port?: number; ssl?: { enabled?: boolean; cert?: string } }
DeepReadonly — ネストしたオブジェクトも再帰的にreadonlyにする
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends (infer Item)[]
? ReadonlyArray<DeepReadonly<Item>>
: T[K] extends object
? T[K] extends (...args: never[]) => unknown
? T[K] // 関数はそのまま
: DeepReadonly<T[K]>
: T[K]
}
const config: DeepReadonly<Config> = { ... }
config.server.port = 3001 // ❌ Error: readonly
config.server.ssl.enabled = false // ❌ Error: ネストも保護
Awaited(自前実装)— Promiseを再帰的に展開する
type MyAwaited<T> =
T extends null | undefined ? T
: T extends object & { then(onfulfilled: infer F): unknown }
? F extends (value: infer V) => unknown
? MyAwaited<V>
: never
: T
type A = MyAwaited<Promise<string>> // string
type B = MyAwaited<Promise<Promise<number>>> // number
9-4. 高度なユーティリティ型パターン
Flatten — ネストした配列を平坦化する型
type Flatten<T> = T extends (infer Item)[]
? Item extends unknown[]
? Flatten<Item>
: Item
: T
type A = Flatten<number[][]> // number
type B = Flatten<string[][][]> // string
type C = Flatten<string> // string(配列でないのでそのまま)
UnionToIntersection — ユニオン型をインターセクション型に変換
// 関数の引数の反変性を利用
type UnionToIntersection<U> =
(U extends unknown ? (arg: U) => void : never) extends
(arg: infer I) => void ? I : never
type A = UnionToIntersection<{ a: string } | { b: number }>
// { a: string } & { b: number }
TupleToUnion — タプル型をユニオム型に変換
type TupleToUnion<T extends readonly unknown[]> = T[number]
type Colors = readonly ['red', 'green', 'blue']
type Color = TupleToUnion<Colors> // 'red' | 'green' | 'blue'
PickByValue — 値の型でプロパティを選ぶ
type PickByValue<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K]
}
type User = {
id: number
name: string
email: string
age: number
active: boolean
}
type StringProps = PickByValue<User, string> // { name: string; email: string }
type NumberProps = PickByValue<User, number> // { id: number; age: number }
Mutable — readonlyを再帰的に外す
type Mutable<T> = {
-readonly [K in keyof T]: T[K] extends object
? T[K] extends (...args: never[]) => unknown
? T[K]
: Mutable<T[K]>
: T[K]
}
type ImmutableUser = Readonly<{ id: number; name: string; address: Readonly<{ city: string }> }>
type MutableUser = Mutable<ImmutableUser>
// { id: number; name: string; address: { city: string } }(readonlyが外れる)
Split — 文字列を区切り文字で分割する型
type Split<S extends string, D extends string> =
S extends `${infer Before}${D}${infer After}`
? [Before, ...Split<After, D>]
: [S]
type Parts = Split<'a,b,c,d', ','>
// ['a', 'b', 'c', 'd']
type PathParts = Split<'users/123/posts', '/'>
// ['users', '123', 'posts']
9-5. 9-X. このセクションのまとめ
よく使う組み込みユーティリティ型:
Partial<T> → 全プロパティをオプショナルに
Required<T> → 全オプショナルを必須に
Readonly<T> → 全プロパティをreadonlyに
Pick<T, K> → 特定のプロパティだけ取り出す
Omit<T, K> → 特定のプロパティを除く
Record<K, V> → キーと値の型からオブジェクト型
Exclude<T, U> → TからUを除く(ユニオン用)
Extract<T, U> → TからUだけ取り出す(ユニオン用)
NonNullable<T> → null/undefinedを除く
ReturnType<T> → 関数の戻り値型
Parameters<T> → 関数の引数型(タプル)
Awaited<T> → Promiseの解決型
自前ユーティリティ型:
DeepPartial<T> → 再帰的にオプショナル化
DeepReadonly<T> → 再帰的にreadonly化
実務的な組み合わせ:
Omit<T, keyof BaseEntity> → 基底型のキーを一括除外
Partial<Omit<T, 'id'>> & { id: number } → 更新用Input型
10. Mapped TypesとConditional Types
このセクションではTypeScriptの型システムで最も強力な2つの機能を解説します。Mapped Typesは「型のすべてのプロパティを変換する」、Conditional Typesは「型レベルのif/else」です。これらを使いこなすことで、手書きでは追いつかない高度な型変換が自動化できます。
10-1. Mapped Types(マップ型)の基本
// Mapped Typeの基本構文
type MappedType<T> = {
[K in keyof T]: T[K] // 各キーKに対してT[K] の型
}
// 実用例: nullable版を作る
type Nullable<T> = {
[K in keyof T]: T[K] | null
}
// 修飾子の追加と削除
type ReadonlyOptional<T> = {
readonly [K in keyof T]?: T[K]
}
// -readonlyでreadonlyを外す
type Mutable<T> = {
-readonly [K in keyof T]: T[K]
}
// -? でオプショナルを外す
type AllRequired<T> = {
[K in keyof T]-?: T[K]
}
10-2. キーの再マッピング(Key Remapping, TS4.1+)
// as句を使ってキーを変換
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
type UserGetters = Getters<{ name: string; age: number }>
// { getName: () => string; getAge: () => number }
// フィルタリング: 特定の型のキーのみ取り出す
type StringKeys<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
}
type StringUserProps = StringKeys<User>
// nameとemailのみ(id: numberとcreatedAt: Dateは除外)
// オプショナルプロパティのキーのみを取り出す
type OptionalKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? K : never
}[keyof T]
10-3. Conditional Types(条件型)の基本
// 基本構文: T extends U ? X : Y
type IsString<T> = T extends string ? true : false
type A = IsString<'hello'> // true
type B = IsString<42> // false
// 型の分類
type TypeName<T> =
T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
T extends null ? 'null' :
T extends undefined ? 'undefined' :
T extends Function ? 'function' :
'object'
type E = TypeName<string> // 'string'
type F = TypeName<42> // 'number'
type G = TypeName<() => void> // 'function'
10-4. infer を使った型の抽出
// 配列の要素型を取り出す
type ArrayElement<T> = T extends (infer Item)[] ? Item : never
type StrEl = ArrayElement<string[]> // string
// 関数の戻り値型を取り出す
type ReturnType<T> = T extends (...args: never[]) => infer R ? R : never
// タプルの最初の要素
type Head<T extends readonly unknown[]> =
T extends readonly [infer H, ...unknown[]] ? H : never
type H = Head<[string, number, boolean]> // string
// タプルの残りの要素
type Tail<T extends readonly unknown[]> =
T extends readonly [unknown, ...infer Rest] ? Rest : never
type Rest = Tail<[string, number, boolean]> // [number, boolean]
10-5. 分散条件型(Distributive Conditional Types)
// ユニオン型に条件型を適用すると、各メンバーに分散して適用される
type ToArray<T> = T extends unknown ? T[] : never
type Result = ToArray<string | number>
// string[] | number[]
// 分散を利用したフィルタリング
type FilterOut<T, U> = T extends U ? never : T
type WithoutNumbers = FilterOut<string | number | boolean, number>
// string | boolean
// 分散を防ぐ(タプルでラップ)
type NonDistributive<T> = [T] extends [unknown] ? T[] : never
type Result2 = NonDistributive<string | number>
// (string | number)[](分散しない)
10-6. Mapped TypesとConditional Typesの組み合わせ
// 関数プロパティだけを取り出す型
type FunctionProperties<T> = {
[K in keyof T as T[K] extends Function ? K : never]: T[K]
}
class UserService {
userId: number = 0
getUser(): User { return {} as User }
createUser(input: CreateUserInput): Promise<User> { return {} as Promise<User> }
}
type ServiceMethods = FunctionProperties<UserService>
// { getUser: () => User; createUser: (input: CreateUserInput) => Promise<User> }
// 必須プロパティのキーのみを取り出す
type OptionalKeys<T> = {
[K in keyof T]-?: undefined extends T[K] ? K : never
}[keyof T]
type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>
type UserRequiredKeys = RequiredKeys<{ id: number; name: string; email?: string }>
// 'id' | 'name'
10-7. 10-X. このセクションのまとめ
Mapped Types:
[K in keyof T]: T[K] → 全プロパティを変換
readonly/? の追加と削除(-readonly, -?)
as句でキーを変換(key remapping)
as neverでキーを除外(フィルタリング)
Conditional Types:
T extends U ? X : Y → 型レベルのif/else
分散: ユニオン型の各メンバーに自動適用
[T] extends [U] でラップ → 分散を防ぐ
infer:
条件型の中で型の一部を取り出す
infer R, infer Item, infer Hなど
実務的な組み合わせ:
StringKeys<T> → 文字列型プロパティのみ取り出す
FunctionProperties<T> → 関数プロパティのみ取り出す
OptionalKeys<T> → オプショナルキーのみ取り出す
11. Template Literal Types
このセクションではTypeScript 4.1+ で追加されたTemplate Literal Typesを解説します。JavaScriptのテンプレートリテラル(`Hello, ${name}`)を型レベルに持ち込んだ機能で、文字列パターンの型安全な表現が可能になります。
11-1. 基本構文
// 基本: 文字列リテラル型の結合
type Greeting = `Hello, ${string}!`
const g: Greeting = 'Hello, World!' // ✅
const invalid: Greeting = 'Hi there!' // ❌ Error
// ユニオンとの組み合わせ(直積展開)
type EventName = `on${Capitalize<'click' | 'focus' | 'blur'>}`
// 'onClick' | 'onFocus' | 'onBlur'
type Direction = 'top' | 'right' | 'bottom' | 'left'
type CSSProperty = `margin-${Direction}` | `padding-${Direction}`
// 'margin-top' | 'margin-right' | ... | 'padding-top' | ...(8通り)
11-2. 組み込み文字列操作型
// TypeScript組み込みの文字列操作型
type A = Uppercase<'hello'> // 'HELLO'
type B = Lowercase<'HELLO'> // 'hello'
type C = Capitalize<'hello'> // 'Hello'
type D = Uncapitalize<'Hello'> // 'hello'
// 実用例: CSS値の型
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vw' | 'vh'
type CSSValue = `${number}${CSSUnit}`
function setWidth(width: CSSValue) {
document.body.style.width = width
}
setWidth('100px') // ✅
setWidth('50%') // ✅
setWidth('50vw') // ✅
setWidth('50xy') // ❌ Error: 'xy' はCSSUnitにない
11-3. 実用パターン
// APIエンドポイントの型安全
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = '/users' | '/posts' | '/comments'
type ApiCall = `${Method} ${Endpoint}`
// 'GET /users' | 'GET /posts' | 'POST /users' | ... (12通り)
// イベントリスナーの型
type EventHandlers<T extends string> = {
[K in T as `on${Capitalize<K>}`]?: () => void
}
type ButtonEvents = EventHandlers<'click' | 'focus' | 'blur'>
// { onClick?: () => void; onFocus?: () => void; onBlur?: () => void }
// CamelCase変換型
type CamelCase<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize<CamelCase<Tail>>}`
: S
type CC = CamelCase<'hello_world_foo'> // 'helloWorldFoo'
// snake_case → camelCase変換(オブジェクト型のキーを変換)
type CamelCaseKeys<T> = {
[K in keyof T as CamelCase<string & K>]: T[K]
}
type SnakeUser = { user_id: number; first_name: string; created_at: Date }
type CamelUser = CamelCaseKeys<SnakeUser>
// { userId: number; firstName: string; createdAt: Date }
11-4. Template Literal Typesの実用パターン集
REST APIのエンドポイント型
// HTTPメソッドとパスの組み合わせを型で管理
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
type ApiPath = '/users' | '/users/:id' | '/posts' | '/posts/:id' | '/auth/login'
type ApiEndpoint = `${HttpMethod} ${ApiPath}`
// 'GET /users' | 'GET /users/:id' | 'POST /users' | ... (25通り)
// ルートパラメータのパース
type ExtractParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractParams<Rest>]: string }
: Path extends `${string}:${infer Param}`
? { [K in Param]: string }
: Record<never, never>
type UserParams = ExtractParams<'/users/:userId/posts/:postId'>
// { userId: string; postId: string }
type EmptyParams = ExtractParams<'/users'>
// Record<never, never>(パラメータなし)
イベント名のマッピング
// DOMイベント名のマッピング(ブラウザ標準)
type DomEvent = 'click' | 'focus' | 'blur' | 'change' | 'input' | 'submit'
type EventHandler<E extends DomEvent> = `on${Capitalize<E>}`
type ClickHandler = EventHandler<'click'> // 'onClick'
type ChangeHandler = EventHandler<'change'> // 'onChange'
// コンポーネントのPropsからイベントハンドラを生成
type ComponentEvents = 'select' | 'deselect' | 'toggle'
type EventHandlerProps = {
[E in ComponentEvents as `on${Capitalize<E>}`]?: (event: { type: E }) => void
}
// { onSelect?: ...; onDeselect?: ...; onToggle?: ... }
CSS-in-JSの型安全
// CSSプロパティと値の型安全な定義
type TimeUnit = 'ms' | 's'
type DurationValue = `${number}${TimeUnit}`
type EasingFunction =
| 'linear'
| 'ease'
| 'ease-in'
| 'ease-out'
| 'ease-in-out'
| `cubic-bezier(${number}, ${number}, ${number}, ${number})`
type TransitionProperty = string // CSSプロパティ名
type Transition = `${TransitionProperty} ${DurationValue}` | `${TransitionProperty} ${DurationValue} ${EasingFunction}`
function createAnimation(transition: Transition): CSSProperties {
return { transition }
}
createAnimation('opacity 300ms ease-out') // ✅
createAnimation('transform 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94)') // ✅
11-5. 11-X. このセクションのまとめ
Template Literal Types:
`${Type}` → 型レベルのテンプレートリテラル
ユニオン型との組み合わせで直積展開される
組み込み文字列操作型:
Uppercase<S>, Lowercase<S>
Capitalize<S>, Uncapitalize<S>
実用パターン:
CSSValue = `${number}${CSSUnit}` → 数値+単位の型安全な文字列
EventName = `on${Capitalize<Event>}` → イベントハンドラ名の生成
CamelCase<S> → inferでsnake_caseをcamelCaseに変換
12. 関数の型
このセクションではTypeScriptにおける関数の型付けを詳しく解説します。関数型の定義方法から、オーバーロード、型の互換性まで、実務で必要な知識を体系的にまとめます。
12-1. 関数型の書き方
// 関数型エイリアス
type Add = (a: number, b: number) => number
type Callback = (error: Error | null, result?: string) => void
type AsyncFn<T> = (...args: unknown[]) => Promise<T>
// インターフェイスでの関数シグネチャ
interface Formatter {
(value: number): string // 呼び出しシグネチャ
format(value: number): string // メソッド
readonly version: string // プロパティも持てる
}
12-2. オーバーロード(関数の多重定義)
オーバーロードは「引数の型によって戻り値の型が変わる」関数を型安全に定義する方法です。
// オーバーロードシグネチャ(実装は最後の1つ)
function createElement(tag: 'div'): HTMLDivElement
function createElement(tag: 'span'): HTMLSpanElement
function createElement(tag: 'input'): HTMLInputElement
function createElement(tag: string): HTMLElement {
// 実装は最も広い型で書く
return document.createElement(tag)
}
const div = createElement('div') // 型: HTMLDivElement
const span = createElement('span') // 型: HTMLSpanElement
// 引数の数が変わるオーバーロード
function log(message: string): void
function log(level: 'info' | 'warn' | 'error', message: string): void
function log(levelOrMessage: string, message?: string): void {
if (message === undefined) {
console.log(levelOrMessage)
} else {
console[levelOrMessage as 'info' | 'warn' | 'error'](message)
}
}
log('hello') // ✅
log('info', 'hello') // ✅
log('info') // ❌ Error: この形は定義されていない
12-3. 関数型の互換性
// 引数の数: 少ない引数の関数は多い引数の関数として使える
type Handler = (event: Event, context: Context) => void
const simpleHandler: Handler = (event) => { // contextを無視してもOK
console.log(event.type)
}
// なぜ: JavaScriptでは引数を無視することが一般的(Array.mapのコールバックなど)
[1, 2, 3].map(n => n * 2)
// mapのコールバックの型は (value: number, index: number, array: number[]) => number
// でもn => n * 2はindex, arrayを無視している(よくあるパターン)
// 戻り値の型: voidを返す関数は戻り値を返す関数として使える(戻り値が無視される)
type VoidFn = () => void
const returnsString: VoidFn = () => 'hello' // ✅ 戻り値は無視される
// ❌ ただし、戻り値が型として活用される場合はエラーになる
const result: void = returnsString() // ✅(voidとして扱われる)
12-4. 高階関数とジェネリクス
// Buildersパターン: メソッドチェーンで型が積み上がる
class QueryBuilder<T extends Record<string, unknown>> {
private filters: Partial<T> = {}
where<K extends keyof T>(key: K, value: T[K]): this {
this.filters[key] = value
return this
}
build(): Partial<T> {
return this.filters
}
}
type UserFilter = { name: string; age: number; role: string }
const query = new QueryBuilder<UserFilter>()
.where('name', 'Alice') // K='name', value: string ✅
.where('age', 30) // K='age', value: number ✅
.where('age', 'thirty') // ❌ Error: stringはnumberに代入できない
.build()
// 関数合成の型
function pipe<A, B, C>(
fn1: (a: A) => B,
fn2: (b: B) => C
): (a: A) => C {
return (a) => fn2(fn1(a))
}
const processUser = pipe(
(user: User) => user.name, // User → string
(name: string) => name.trim() // string → string
)
// 型: (user: User) => string
12-5. 可変長引数の型
// restパラメータの型
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0)
}
// タプルを使った型安全な可変長引数
function first<T extends unknown[]>(...args: T): T[0] {
return args[0]
}
const f = first(1, 'hello', true) // 型: number
// Variadic Tuple Types(TS4.0+)
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U]
type AB = Concat<[string, number], [boolean, Date]>
// [string, number, boolean, Date]
// 関数の引数をタプルで操作
type PrependParam<F extends (...args: unknown[]) => unknown, T> =
F extends (...args: infer Args) => infer R
? (first: T, ...args: Args) => R
: never
type OrigFn = (a: string, b: number) => boolean
type WithId = PrependParam<OrigFn, number>
// (first: number, a: string, b: number) => boolean
12-6. 関数型プログラミングパターン
TypeScriptの型システムは関数型プログラミングのパターンとの相性が非常に良いです。
// Option/Maybeモナドパターン
type None = { readonly _tag: 'None' }
type Some<A> = { readonly _tag: 'Some'; readonly value: A }
type Option<A> = None | Some<A>
const none: None = { _tag: 'None' }
const some = <A>(value: A): Some<A> => ({ _tag: 'Some', value })
// Optionのユーティリティ関数
function map<A, B>(option: Option<A>, fn: (a: A) => B): Option<B> {
if (option._tag === 'None') return none
return some(fn(option.value))
}
function flatMap<A, B>(option: Option<A>, fn: (a: A) => Option<B>): Option<B> {
if (option._tag === 'None') return none
return fn(option.value)
}
function getOrElse<A>(option: Option<A>, defaultValue: A): A {
if (option._tag === 'None') return defaultValue
return option.value
}
// 使用例
function findUser(id: number): Option<User> {
const user = users.find(u => u.id === id)
return user ? some(user) : none
}
const userName = getOrElse(
map(findUser(1), user => user.name),
'不明なユーザー'
)
// パイプライン演算子の代替(compose関数)
function compose<A, B, C>(
f: (b: B) => C,
g: (a: A) => B
): (a: A) => C {
return a => f(g(a))
}
function pipe<A, B, C, D>(
fn1: (a: A) => B,
fn2: (b: B) => C,
fn3: (c: C) => D
): (a: A) => D {
return a => fn3(fn2(fn1(a)))
}
const processUserName = pipe(
(id: number) => findUser(id),
(user: Option<User>) => map(user, u => u.name),
(name: Option<string>) => getOrElse(name, '不明')
)
const result = processUserName(1) // string
12-7. thisの型注釈
// TypeScriptではthisの型を明示できる
interface Logger {
prefix: string
log(this: Logger, message: string): void
}
// thisの型を制限することでコンテキスト外での呼び出しを防ぐ
function log(this: Logger, message: string): void {
console.log(`[${this.prefix}] ${message}`)
}
const logger: Logger = {
prefix: 'INFO',
log
}
logger.log('テストメッセージ') // ✅
const standalone = logger.log
// standalone('メッセージ') // ❌ Error: thisがLoggerでない
// メソッドチェーンのためのthis型
class Builder {
private items: string[] = []
add(item: string): this { // thisを返すことでサブクラスでも型が保持される
this.items.push(item)
return this
}
build(): string[] {
return this.items
}
}
class ExtendedBuilder extends Builder {
addMultiple(...items: string[]): this {
items.forEach(item => this.add(item))
return this
}
}
const result = new ExtendedBuilder()
.add('a')
.addMultiple('b', 'c') // this型があるのでチェーンが保持される
.add('d')
.build()
12-8. 12-X. このセクションのまとめ
関数型の定義:
type Add = (a: number, b: number) => number(型エイリアス)
interfaceに呼び出しシグネチャを持たせることもできる
オーバーロード:
引数/戻り値の型が複数パターンある関数に使う
シグネチャを複数書き、最後に実装を書く
実装シグネチャは最も広い型にする
関数型の互換性:
引数が少ない関数は多い引数の関数として使える(引数の無視)
voidを返す関数型は実際に値を返す関数を受け入れる
高階関数:
ジェネリクスで型を引き継ぐ関数合成が可能
pipe/composeパターンで型安全な変換を連鎖できる
13. クラスと型
このセクションではTypeScriptのクラス機能と型システムの関係を解説します。クラスはコンストラクタ関数の糖衣構文ですが、TypeScriptではアクセス修飾子、抽象クラス、実装宣言など追加機能があります。
13-1. TypeScriptクラスの全機能
abstract class Repository<T extends { id: number }> {
// アクセス修飾子
public name: string // デフォルト(publicは省略可)
protected items: Map<number, T> = new Map()
private readonly createdAt: Date
// コンストラクタショートハンド(宣言と代入を1行で)
constructor(
public readonly tableName: string, // publicでプロパティとして宣言
protected logger: Logger // protectedで宣言
) {
this.name = tableName
this.createdAt = new Date()
}
// 抽象メソッド(サブクラスで実装必須)
abstract findById(id: number): Promise<T | null>
// 具象メソッド
async findAll(): Promise<T[]> {
return Array.from(this.items.values())
}
// 静的メソッド
static create<U extends { id: number }>(tableName: string): void {
throw new Error('抽象クラスは直接使えません')
}
}
class UserRepository extends Repository<User> {
async findById(id: number): Promise<User | null> {
return this.items.get(id) ?? null
}
async findByEmail(email: string): Promise<User | null> {
for (const user of this.items.values()) {
if (user.email === email) return user
}
return null
}
}
13-2. インターフェイスを実装するクラス
interface Serializable {
serialize(): string
}
interface Validatable {
validate(): boolean
}
class UserModel implements Serializable, Validatable {
constructor(private data: Partial<User>) {}
serialize(): string {
return JSON.stringify(this.data)
}
validate(): boolean {
return !!(this.data.name && this.data.email)
}
}
13-3. アクセス修飾子の使い分け
| 修飾子 | クラス内 | サブクラス | クラス外 | 用途 |
|---|---|---|---|---|
public |
✅ | ✅ | ✅ | デフォルト。外部からアクセス可能 |
protected |
✅ | ✅ | ❌ | サブクラスに継承させるが外部には非公開 |
private |
✅ | ❌ | ❌ | 完全に非公開 |
# (ES private) |
✅ | ❌ | ❌ | ランタイムでも本当に非公開(JS標準) |
readonly |
読取 | 読取 | 読取 | 代入不可(コンストラクタのみ設定可) |
class BankAccount {
#balance: number // JSのプライベートフィールド(ランタイムでも非公開)
private _history: Transaction[] = [] // TSのprivate(コンパイル後は普通のプロパティ)
constructor(initialBalance: number) {
this.#balance = initialBalance
}
get balance(): number {
return this.#balance
}
deposit(amount: number): void {
if (amount <= 0) throw new Error('正の金額を指定してください')
this.#balance += amount
this._history.push({ type: 'deposit', amount, timestamp: new Date() })
}
}
13-4. Mixinパターン
TypeScriptのクラスは単一継承ですが、Mixinパターンを使うことで複数の機能を組み合わせられます。
// Mixinの型定義
type Constructor<T = object> = new (...args: unknown[]) => T
// Timestampable Mixin: タイムスタンプ機能を追加
function Timestampable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt: Date = new Date()
updatedAt: Date = new Date()
touch() {
this.updatedAt = new Date()
}
}
}
// Serializable Mixin: JSONシリアライズ機能を追加
function Serializable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
serialize(): string {
return JSON.stringify(this)
}
static deserialize<T>(this: Constructor<T>, json: string): T {
return JSON.parse(json) as T
}
}
}
// 基底クラス
class Entity {
constructor(public readonly id: number) {}
}
// Mixinを適用(複数の機能を合成)
const TimestampedEntity = Timestampable(Entity)
const FullFeaturedEntity = Serializable(TimestampedEntity)
class User extends FullFeaturedEntity {
constructor(id: number, public name: string) {
super(id)
}
}
const user = new User(1, 'Alice')
user.touch() // updatedAtを更新
const json = user.serialize() // JSON文字列
console.log(user.createdAt) // Date
13-5. 13-X. このセクションのまとめ
クラスの型機能:
public / protected / private → アクセス制御
readonly → コンストラクタ後は変更不可
abstract → サブクラスで実装必須
implements → インターフェイスを満たすことを宣言
コンストラクタショートハンド:
constructor(public readonly name: string) {}
→ プロパティ宣言と代入を1行で書ける
structuralTypingとクラス:
TypeScriptはクラス名でなく構造で型チェックする
同じプロパティ/メソッドを持つオブジェクトはクラスの型に代入可能
#field(JSプライベート)vs private:
#field → ランタイムでも非公開(本当のカプセル化)
private → コンパイル時のみ(JSにはなくなる)
14. モジュールと宣言ファイル(.d.ts)
このセクションではTypeScriptのモジュールシステムと、型定義ファイル(.d.ts)について解説します。大規模プロジェクトでのモジュール設計と、型のないJSライブラリへの型付けに必要な知識です。
14-1. モジュールの型
// ファイルにimport/exportがあればモジュール(各ファイルが独立したスコープ)
// なければスクリプト(グローバルスコープ)
// 名前空間宣言(モジュールでない場合のグローバル汚染防止)
declare global {
interface Window {
myLib: {
version: string
}
}
}
14-2. .d.ts宣言ファイル
型定義だけを提供するファイルです。JSライブラリに型を付けるのに使います。
// types/express-extension.d.ts
declare module 'express' {
interface Request {
user?: {
id: string
role: string
}
}
}
// 独自モジュールの宣言(ファイルタイプごとに)
declare module '*.svg' {
const content: string
export default content
}
declare module '*.png' {
const content: string
export default content
}
// グローバル変数の宣言(ビルドツールが注入する変数など)
declare const __APP_VERSION__: string
declare const __DEV__: boolean
// 型定義がないJSライブラリに自前で型を付ける
declare module 'some-untyped-library' {
export function doSomething(input: string): number
export interface Config {
timeout: number
retries?: number
}
export default function init(config: Config): void
}
14-3. 型のインポート・エクスポート
// import type: 型だけをインポート(ランタイムに残らない)
import type { User, Post } from './types'
import type { FC, ReactNode } from 'react'
// export type: 型だけをエクスポート
export type { User, Post }
export type { CreateUserInput } from './types'
// 型と値を混在させる
import { createUser, type User } from './user-service'
// なぜimport typeを使うか
// 1. バンドルサイズ削減: 型だけのimportはビルド後に完全に消える
// 2. 循環参照の回避: 型のみのimportは循環依存にカウントされない
// 3. isolatedModulesとの互換性: type erasableなimportであることが明確
14-4. 実務的な .d.tsの書き方
ライブラリの型定義を拡張する実例
// 1. ExpressのRequestに認証情報を追加
// types/express.d.ts
declare module 'express-serve-static-core' {
interface Request {
user?: {
id: string
email: string
role: 'admin' | 'user' | 'guest'
}
requestId: string // ミドルウェアで付与するID
}
}
// 2. Prismaクライアントにカスタムメソッドを追加(型レベル)
// types/prisma.d.ts
import { PrismaClient } from '@prisma/client'
declare module '@prisma/client' {
interface PrismaClient {
$transaction<T>(fn: (prisma: PrismaClient) => Promise<T>): Promise<T>
}
}
// 3. process.envの型を強化
// types/env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'test' | 'production'
DATABASE_URL: string
PORT?: string
JWT_SECRET: string
}
}
// これによりprocess.env.NODE_ENVがstringでなく
// 'development' | 'test' | 'production' として扱われる
if (process.env.NODE_ENV === 'production') {
// ...
}
// 4. グローバル関数の型宣言(ブラウザ拡張API等)
// types/global.d.ts
declare function gtag(
command: 'event',
action: string,
parameters?: Record<string, unknown>
): void
declare function gtag(
command: 'config',
targetId: string,
parameters?: Record<string, unknown>
): void
// 5. CSSモジュールの型
// types/css-modules.d.ts
declare module '*.module.css' {
const classes: Readonly<Record<string, string>>
export default classes
}
declare module '*.module.scss' {
const classes: Readonly<Record<string, string>>
export default classes
}
// 使う側
import styles from './Button.module.css'
const className = styles.primary // string型(補完なし)
// より精密にしたい場合はcss-modules-typescript-loaderを使う
14-5. 14-X. このセクションのまとめ
.d.tsファイルの役割:
型定義のみ(実装なし)
JSライブラリへの型付け
グローバル変数/モジュールの型宣言
宣言マージの実用:
declare module 'express' { interface Request { user?: ... } }
→ ライブラリの型を拡張
import type / export type:
ランタイムに残らない型専用インポート
バンドルサイズ削減・循環参照回避に有効
@typesパッケージ:
npm install -D @types/nodeなどで型定義を追加
型定義がない場合は自前の .d.tsを書く
15. strictモードと主要コンパイラオプション
このセクションではTypeScriptのコンパイラオプション、特にstrict関連の各フラグを個別に解説します。「strict: trueにしたらエラーが大量に出た」という経験をした方が、なぜそのエラーが起きるのか・どう直すべきかを理解できるようにします。
15-1. strictモードの全フラグ解説
“strict”: trueは以下のフラグをまとめて有効にします。
strictNullChecks — null/undefinedを型として分離する
// strictNullChecks: false(デフォルトの危険な状態)
function getUser(id: number): User {
if (id <= 0) return null // ✅ nullを返してもエラーにならない
}
const user = getUser(-1)
user.name // ランタイムエラー: Cannot read properties of null
// strictNullChecks: true
function getUser(id: number): User | null {
if (id <= 0) return null // ✅ 戻り値型にnullを含める
}
const user = getUser(-1)
user.name // ❌ Error: userがnullかもしれない
user?.name // ✅ optional chainingで安全にアクセス
noImplicitAny — 暗黙的anyを禁止する
// noImplicitAny: false(危険)
function process(value) { // valueは暗黙的にany
return value.trim() // ランタイムエラーの可能性
}
// noImplicitAny: true
function process(value) { // ❌ Error: パラメータ 'value' はany型
function process(value: string) { // ✅
return value.trim()
}
strictFunctionTypes — 関数型の引数に反変チェックを適用する
// strictFunctionTypes: trueで有効になる反変チェック
type Handler = (event: MouseEvent) => void
// MouseEventはEventのサブタイプ(より具体的)
// Eventを受け取る関数はMouseEventを受け取る関数として使えるが、逆はNG
const generalHandler: Handler = (event: Event) => {} // ✅(反変)
const specificHandler: (event: MouseEvent) => void = (e: Event) => {} // ✅
// ❌ 危険な方向
const narrowHandler: (event: Event) => void = (e: MouseEvent) => {
e.clientX // Event型が来たらclientXにアクセスできない
}
strictPropertyInitialization — クラスプロパティの初期化チェック
// strictPropertyInitialization: true
class User {
name: string // ❌ Error: 'name' は初期化されていない
// 解決方法1: constructorで初期化
name: string
constructor() { this.name = '' }
// 解決方法2: 型定義でundefinedを許可
name: string | undefined
// 解決方法3: 明確な代入アサーション(後で確実に代入される場合)
name!: string // ! は「後で必ず代入される」という意思表示
}
useUnknownInCatchVariables — catch節をunknownにする(TS4.4+)
// useUnknownInCatchVariables: false(古い動作)
try {
riskyOperation()
} catch (error) {
error.message // errorはany → 型チェックなし
}
// useUnknownInCatchVariables: true(strictで有効)
try {
riskyOperation()
} catch (error: unknown) {
// error.message // ❌ Error: unknownは操作できない
if (error instanceof Error) {
error.message // ✅ 型チェック後
}
}
15-2. 追加の品質向上オプション
noUncheckedIndexedAccess — 配列アクセスにundefinedを加える
// noUncheckedIndexedAccess: false(デフォルト)
const arr: string[] = ['a', 'b', 'c']
const first: string = arr[0] // 型: string(実際は存在しないかも)
// noUncheckedIndexedAccess: true
const first: string | undefined = arr[0] // 型: string | undefined
if (first !== undefined) {
first.toUpperCase() // ✅ string確定
}
// Record/インデックスシグネチャも同様
const map: Record<string, number> = { a: 1 }
const val: number | undefined = map['b'] // undefinedが含まれる
exactOptionalPropertyTypes — オプショナルとundefinedを区別する
type Config = { timeout?: number }
// exactOptionalPropertyTypes: false
const c1: Config = { timeout: undefined } // ✅(? とundefinedを同一視)
// exactOptionalPropertyTypes: true
const c2: Config = { timeout: undefined } // ❌ Error
const c3: Config = {} // ✅(プロパティが存在しない)
const c4: Config = { timeout: 1000 } // ✅
noImplicitReturns — 全パスにreturnがない関数をエラーに
// noImplicitReturns: true
function process(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase()
}
// ❌ Error: numberの場合にreturnがない
}
// ✅
function process(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase()
}
return String(value)
}
noFallthroughCasesInSwitch — switchのフォールスルーをエラーに
// noFallthroughCasesInSwitch: true
function getLabel(status: string): string {
switch (status) {
case 'active':
return 'アクティブ'
case 'pending':
// ❌ Error: caseがフォールスルーしている(break/returnがない)
case 'inactive':
return '非アクティブ'
}
return '不明'
}
// ✅ 意図的なフォールスルーはcommentで明示
switch (status) {
case 'pending':
// falls through
case 'inactive':
return '非アクティブ'
}
15-3. 設定の選択ガイド
| オプション | 推奨度 | 理由 |
|---|---|---|
| strict: true | 必須 | 型安全の基盤。新規プロジェクトは必ず有効化 |
noUncheckedIndexedAccess |
強推奨 | 配列アクセスのランタイムエラーを防ぐ |
noImplicitReturns |
推奨 | returnし忘れのバグを防ぐ |
noFallthroughCasesInSwitch |
推奨 | switchのバグを防ぐ |
exactOptionalPropertyTypes |
中規模以上で推奨 | オプショナルの意味を厳密に保つ |
strictNullChecks(strictの一部) |
必須 | null/undefinedを安全に扱う |
15-4. 実際のtsconfig設定例(用途別)
フロントエンド(React + Vite)
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
// バンドラーモード
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
// 厳格設定
"strict": true,
"noUnusedLocals": true, // 未使用のローカル変数をエラーに
"noUnusedParameters": true, // 未使用のパラメータをエラーに
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
// パスエイリアス
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
バックエンド(Node.js + Express)
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
// 出力設定
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// 厳格設定
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
// 実験的機能
"experimentalDecorators": true, // NestJSを使う場合
"emitDecoratorMetadata": true // NestJSを使う場合
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}
ライブラリ開発
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020"],
// 型定義ファイルの生成
"declaration": true,
"declarationMap": true, // ソースマップ付き .d.ts
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
// 最も厳格な設定
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
// ライブラリ公開時に重要
"skipLibCheck": false // 自分の型定義の品質を保証する
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
15-5. 15-X. このセクションのまとめ
strict: trueが有効にするフラグ:
strictNullChecks → null/undefinedを型で分離
noImplicitAny → 暗黙的anyを禁止
strictFunctionTypes → 関数引数の反変チェック
strictPropertyInitialization → クラスプロパティの初期化チェック
useUnknownInCatchVariables → catch節をunknownに
追加推奨オプション:
noUncheckedIndexedAccess → 配列/インデックスアクセスにundefined
exactOptionalPropertyTypes → ? とundefinedの厳密な区別
noImplicitReturns → 全パスにreturnを強制
noFallthroughCasesInSwitch → switchフォールスルーを防ぐ
移行時のヒント:
既存プロジェクトでstrictを有効化する場合は
一つずつフラグを有効にして段階的に修正する
16. 非同期処理の型
このセクションではTypeScriptでの非同期処理の型付けを解説します。async/await、Promise、ジェネリクスを組み合わせることで、非同期コードを型安全に書けるようになります。
16-1. async/awaitの型
// async関数は常にPromiseを返す
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) throw new Error(`HTTP Error: ${response.status}`)
return response.json() // Promise<any> → Userとして扱われる(危険)
}
// より安全な型付け(unknownを経由してバリデーション)
async function getUserSafe(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) throw new Error(`HTTP Error: ${response.status}`)
const data: unknown = await response.json()
// Zodなどでバリデーション(セクション24参照)
if (!isUser(data)) throw new TypeError('不正なレスポンス形式')
return data
}
// Promise.allの型
const [user, posts] = await Promise.all([
getUser(1), // Promise<User>
getPosts(1) // Promise<Post[]>
])
// user: User, posts: Post[]
// Promise.allSettledの型
const results = await Promise.allSettled([getUser(1), getPosts(1)])
results.forEach(result => {
if (result.status === 'fulfilled') {
result.value // User | Post[]
} else {
result.reason // unknown(エラーの型はunknown)
}
})
16-2. 型安全なイベントエミッター
type EventMap = {
'user:created': User
'user:deleted': { id: number }
'error': Error
}
class TypedEventEmitter<Events extends Record<string, unknown>> {
private listeners = new Map<
keyof Events,
Set<(data: Events[keyof Events]) => void>
>()
on<K extends keyof Events>(
event: K,
listener: (data: Events[K]) => void
): this {
const set = this.listeners.get(event) ?? new Set()
set.add(listener as (data: Events[keyof Events]) => void)
this.listeners.set(event, set)
return this
}
off<K extends keyof Events>(
event: K,
listener: (data: Events[K]) => void
): this {
this.listeners.get(event)?.delete(
listener as (data: Events[keyof Events]) => void
)
return this
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners.get(event)?.forEach(fn => fn(data))
}
}
const emitter = new TypedEventEmitter<EventMap>()
emitter.on('user:created', (user) => {
console.log(user.name) // 型: User
})
emitter.emit('user:created', { id: 1, name: 'Alice' }) // ✅
emitter.emit('user:created', { id: 1 }) // ❌ nameが足りない
emitter.emit('unknown:event', {}) // ❌ 未定義のイベント
16-3. Promiseチェーンの型追跡
// Promiseチェーンの型は自動的に追跡される
fetch('/api/users/1')
.then(res => res.json()) // Promise<any>
.then((data: unknown) => {
if (!isUser(data)) throw new Error('invalid')
return data // User
})
.then(user => user.name) // Promise<string>
.catch(error => 'default-name') // Promise<string | 'default-name'>
// async/awaitで同等の型安全さ
async function getUserName(id: number): Promise<string> {
const response = await fetch(`/api/users/${id}`)
const data: unknown = await response.json()
if (!isUser(data)) throw new Error('invalid')
return data.name // string
}
16-4. Promiseの型パターン集
// パターン1: タイムアウト付きのPromise
function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
errorMessage = 'タイムアウトしました'
): Promise<T> {
return Promise.race([
promise,
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(errorMessage)), timeoutMs)
)
])
}
const user = await withTimeout(fetchUser(1), 5000)
// 型: User(Promise<never> とのraceでも型が保持される)
// パターン2: リトライ付きのPromise
async function withRetry<T>(
fn: () => Promise<T>,
retries = 3,
delayMs = 1000
): Promise<T> {
let lastError: unknown
for (let i = 0; i <= retries; i++) {
try {
return await fn()
} catch (error) {
lastError = error
if (i < retries) {
await new Promise(resolve => setTimeout(resolve, delayMs * Math.pow(2, i)))
}
}
}
throw lastError
}
const data = await withRetry(() => fetchUserData(1), 3, 500)
// 型: User(リトライしても型は保持)
// パターン3: Promiseのバッチ処理(並行数制限付き)
async function pLimit<T>(
tasks: (() => Promise<T>)[],
concurrency: number
): Promise<T[]> {
const results: T[] = []
const executing = new Set<Promise<void>>()
for (const task of tasks) {
const p = task().then(result => {
results.push(result)
executing.delete(p as unknown as Promise<void>)
})
executing.add(p as unknown as Promise<void>)
if (executing.size >= concurrency) {
await Promise.race(executing)
}
}
await Promise.all(executing)
return results
}
// 最大3並行でユーザーを取得
const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
const users = await pLimit(
userIds.map(id => () => fetchUser(id)),
3
)
// 型: User[]
// パターン4: キャンセル可能なPromise(AbortController)
async function fetchWithCancel<T>(
url: string,
signal: AbortSignal
): Promise<T> {
const response = await fetch(url, { signal })
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json() as Promise<T>
}
// 使い方(ReactのuseEffectなど)
const controller = new AbortController()
try {
const user = await fetchWithCancel<User>('/api/users/1', controller.signal)
setUser(user)
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
console.log('リクエストがキャンセルされました')
} else {
throw error
}
}
// クリーンアップ時にキャンセル
controller.abort()
16-5. GeneratorとAsyncGeneratorの型
// Generatorの型: Generator<YieldType, ReturnType, NextType>
function* counter(start: number): Generator<number, string, boolean> {
let current = start
let shouldStop = false
while (!shouldStop) {
shouldStop = yield current // yield: booleanを受け取りnumberを返す
current++
}
return `${current} まで数えました`
}
const gen = counter(1)
gen.next() // { value: 1, done: false }
gen.next(false) // { value: 2, done: false }
gen.next(true) // { value: '3まで数えました', done: true }
// AsyncGenerator: 非同期なデータストリーム
async function* fetchPages<T>(
url: string,
pageSize: number
): AsyncGenerator<T[], void, undefined> {
let page = 1
let hasMore = true
while (hasMore) {
const response = await fetch(`${url}?page=${page}&size=${pageSize}`)
const data: { items: T[]; hasMore: boolean } = await response.json()
yield data.items
hasMore = data.hasMore
page++
}
}
// 使い方(for await...of)
for await (const users of fetchPages<User>('/api/users', 10)) {
await processUsers(users) // users: User[]
}
16-6. 16-X. このセクションのまとめ
async/awaitの型:
async関数の戻り値型はPromise<T>
awaitでPromise<T> からTを取り出す
response.json() はPromise<any>(unsafe)→ unknown経由でバリデーション
Promiseユーティリティ:
Promise.all([p1, p2]) → [T1, T2](タプル型に推論)
Promise.allSettled → PromiseSettledResult<T>[] の配列
型安全なイベントエミッター:
EventMap型でイベント名とデータ型をマッピング
on<K extends keyof Events>(event: K, ...) で型安全なリスナー登録
17. エラーハンドリングパターン
このセクションではTypeScriptでの型安全なエラーハンドリングを解説します。JavaScriptの try/catch は型がなく危険ですが、TypeScript 4.0+ の unknown catch変数や Result 型パターンを使うことで安全に扱えます。
17-1. unknown を使った安全なエラーハンドリング
// TypeScript 4.0+ ではcatch句の型はデフォルトunknown(strictモード)
try {
await riskyOperation()
} catch (error: unknown) {
// errorはunknownなので型チェックが必要
if (error instanceof Error) {
console.error(error.message)
console.error(error.stack)
} else if (typeof error === 'string') {
console.error(error)
} else {
console.error('Unknown error:', JSON.stringify(error))
}
}
// エラーハンドリングヘルパー
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message
if (typeof error === 'string') return error
return String(error)
}
// カスタムエラークラス
class AppError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number = 500
) {
super(message)
this.name = 'AppError'
// V8のstack traceを正しく設定
if (Error.captureStackTrace) {
Error.captureStackTrace(this, AppError)
}
}
}
class NotFoundError extends AppError {
constructor(resource: string, id: number | string) {
super(`${resource} (id=${id}) が見つかりません`, 'NOT_FOUND', 404)
}
}
// 型安全なinstanceofチェック
function handleAppError(error: unknown) {
if (error instanceof NotFoundError) {
return { status: 404, message: error.message }
}
if (error instanceof AppError) {
return { status: error.statusCode, message: error.message }
}
return { status: 500, message: getErrorMessage(error) }
}
17-2. Result型パターン
try/catch の代わりに成功・失敗を型で表現するパターンです。エラーが型の中に見えるため、ハンドリング忘れを防げます。
// 成功か失敗かを型で表現
type Ok<T> = { ok: true; value: T }
type Err<E> = { ok: false; error: E }
type Result<T, E = Error> = Ok<T> | Err<E>
// ヘルパー関数
function ok<T>(value: T): Ok<T> {
return { ok: true, value }
}
function err<E>(error: E): Err<E> {
return { ok: false, error }
}
// 使用例
async function fetchUser(id: number): Promise<Result<User>> {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
return err(new Error(`HTTP ${response.status}`))
}
const data = await response.json()
return ok(data as User)
} catch (error) {
return err(error instanceof Error ? error : new Error(String(error)))
}
}
// 使う側(エラーハンドリングを強制される)
const result = await fetchUser(1)
if (result.ok) {
console.log(result.value.name) // 型: User
} else {
console.error(result.error.message) // 型: Error
}
// Resultチェーン
function mapResult<T, U, E>(
result: Result<T, E>,
fn: (value: T) => U
): Result<U, E> {
if (result.ok) return ok(fn(result.value))
return result
}
const nameResult = mapResult(await fetchUser(1), user => user.name)
// Result<string, Error>
17-3. エラー型の設計パターン
// エラーコードとメッセージを型で管理
type ErrorCode =
| 'VALIDATION_ERROR'
| 'NOT_FOUND'
| 'UNAUTHORIZED'
| 'INTERNAL_ERROR'
type AppError =
| { code: 'VALIDATION_ERROR'; field: string; message: string }
| { code: 'NOT_FOUND'; resource: string; id: number }
| { code: 'UNAUTHORIZED'; reason: string }
| { code: 'INTERNAL_ERROR'; message: string; cause?: unknown }
// 判別可能なユニオンでエラーを処理
function handleError(error: AppError): Response {
switch (error.code) {
case 'VALIDATION_ERROR':
return { status: 400, body: `${error.field}: ${error.message}` }
case 'NOT_FOUND':
return { status: 404, body: `${error.resource}(${error.id})が見つかりません` }
case 'UNAUTHORIZED':
return { status: 401, body: error.reason }
case 'INTERNAL_ERROR':
return { status: 500, body: '内部エラーが発生しました' }
default:
return exhaustiveCheck(error)
}
}
17-4. 実務的なエラーハンドリング戦略
エラーの階層化と型設計
// アプリケーション全体のエラー型を設計する
// ベースエラー
abstract class BaseError extends Error {
abstract readonly code: string
abstract readonly statusCode: number
constructor(message: string, public readonly cause?: unknown) {
super(message)
this.name = this.constructor.name
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor)
}
}
toJSON() {
return {
code: this.code,
message: this.message,
...(process.env.NODE_ENV === 'development' && { stack: this.stack })
}
}
}
// 具体的なエラークラス
class ValidationError extends BaseError {
readonly code = 'VALIDATION_ERROR'
readonly statusCode = 400
constructor(
message: string,
public readonly field: string,
public readonly value: unknown
) {
super(message)
}
}
class NotFoundError extends BaseError {
readonly code = 'NOT_FOUND'
readonly statusCode = 404
constructor(resource: string, identifier: string | number) {
super(`${resource} (${identifier}) が見つかりません`)
}
}
class UnauthorizedError extends BaseError {
readonly code = 'UNAUTHORIZED'
readonly statusCode = 401
constructor(message = '認証が必要です') {
super(message)
}
}
class ConflictError extends BaseError {
readonly code = 'CONFLICT'
readonly statusCode = 409
constructor(resource: string, field: string) {
super(`${resource}の${field}は既に使用されています`)
}
}
class InternalError extends BaseError {
readonly code = 'INTERNAL_ERROR'
readonly statusCode = 500
constructor(message = '内部エラーが発生しました', cause?: unknown) {
super(message, cause)
}
}
// エラーの型ユニオン
type AppError =
| ValidationError
| NotFoundError
| UnauthorizedError
| ConflictError
| InternalError
// 型安全なエラーハンドラー
function handleError(error: AppError): { status: number; body: object } {
return {
status: error.statusCode,
body: error.toJSON()
}
}
// Expressミドルウェアでの使用
function errorMiddleware(
error: unknown,
req: Request,
res: Response,
next: NextFunction
): void {
if (error instanceof BaseError) {
res.status(error.statusCode).json(error.toJSON())
return
}
console.error('予期しないエラー:', error)
const internal = new InternalError(undefined, error)
res.status(500).json(internal.toJSON())
}
複数のエラーを集約する
// バリデーションエラーを複数まとめる
class AggregateValidationError extends BaseError {
readonly code = 'VALIDATION_ERROR'
readonly statusCode = 400
constructor(public readonly errors: ValidationError[]) {
super(`${errors.length}件のバリデーションエラー`)
}
toJSON() {
return {
...super.toJSON(),
errors: this.errors.map(e => ({
field: e.field,
message: e.message,
value: e.value
}))
}
}
}
// 複数フィールドのバリデーション
function validateCreateUser(input: unknown): CreateUserInput {
const errors: ValidationError[] = []
if (typeof input !== 'object' || input === null) {
throw new ValidationError('オブジェクトが必要です', 'input', input)
}
const obj = input as Record<string, unknown>
if (typeof obj.name !== 'string' || obj.name.length < 1) {
errors.push(new ValidationError('名前は必須です', 'name', obj.name))
}
if (typeof obj.email !== 'string' || !obj.email.includes('@')) {
errors.push(new ValidationError('メールアドレスが不正です', 'email', obj.email))
}
if (errors.length > 0) {
throw new AggregateValidationError(errors)
}
return obj as CreateUserInput
}
17-5. 17-X. このセクションのまとめ
unknownを使ったエラーハンドリング:
catch (error: unknown) → 型チェック後にのみ操作可能
getErrorMessage(error: unknown): stringヘルパーが便利
カスタムエラークラスでinstanceofチェック
Result型パターン:
type Result<T, E> = Ok<T> | Err<E>
エラーが型に見える → ハンドリング忘れを防ぐ
try/catchより型安全だが、既存コードとの統合に注意
エラー型の設計:
判別可能なユニオンでエラーコードを管理
switchで網羅性チェック
エラーごとに必要な情報を型で定義(field, resource, idなど)
18. 実務パターン集
このセクションでは実際のプロジェクトで役立つTypeScriptの型パターンを集めました。「こういう場面でどう型を書くか」という実務的な観点から解説します。
18-1. Branded Types(ブランド型)
同じプリミティブ型だが意味が異なる値を区別します。
// UserIdとPostIdは両方numberだが混同を防ぐ
declare const __brand: unique symbol
type Brand<T, B> = T & { [__brand]: B }
type UserId = Brand<number, 'UserId'>
type PostId = Brand<number, 'PostId'>
type Email = Brand<string, 'Email'>
function getUser(id: UserId): User { return {} as User }
function getPost(id: PostId): Post { return {} as Post }
const userId = 1 as UserId
const postId = 2 as PostId
getUser(userId) // ✅
getUser(postId) // ❌ Error: PostIdはUserIdに代入できない
getUser(1) // ❌ Error: numberはUserIdに代入できない
// バリデーション付きコンストラクタ
function createEmail(input: string): Email {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input)) {
throw new Error(`無効なメールアドレス: ${input}`)
}
return input as Email
}
// Email型を受け取る関数
function sendEmail(to: Email, subject: string): void {
// toは検証済みのメールアドレス
}
sendEmail(createEmail('alice@example.com'), '件名') // ✅
sendEmail('raw@string.com', '件名') // ❌ 未検証の文字列は渡せない
18-2. Opaque Types(不透明型)
Branded Typesに似ていますが、作成関数を通じてのみ生成できる型です。
// Symbolを使った不透明型(実行時コストなし)
type Opaque<T, Token extends symbol> = T & { readonly __opaque__: Token }
declare const _UserId: unique symbol
declare const _Amount: unique symbol
type UserId = Opaque<number, typeof _UserId>
type Amount = Opaque<number, typeof _Amount>
// Smart Constructor(バリデーション付きの生成関数)
function userId(n: number): UserId {
if (!Number.isInteger(n) || n <= 0) {
throw new Error(`無効なUserId: ${n}`)
}
return n as UserId
}
function amount(n: number): Amount {
if (n < 0) throw new Error('金額は0以上でなければなりません')
return n as Amount
}
function processPayment(userId: UserId, amount: Amount): void {
console.log(`ユーザー${userId}から${amount}円の支払いを処理`)
}
processPayment(userId(1), amount(1000)) // ✅
processPayment(1 as UserId, amount(-100)) // amount(-100) でエラー
18-3. Builderパターン
class QueryBuilder<T> {
private query: {
table: string
conditions: string[]
orderBy?: string
limit?: number
}
constructor(table: string) {
this.query = { table, conditions: [] }
}
where(condition: string): this {
this.query.conditions.push(condition)
return this
}
orderBy(field: keyof T, direction: 'ASC' | 'DESC' = 'ASC'): this {
this.query.orderBy = `${String(field)} ${direction}`
return this
}
take(limit: number): this {
this.query.limit = limit
return this
}
build(): string {
let sql = `SELECT * FROM ${this.query.table}`
if (this.query.conditions.length > 0) {
sql += ` WHERE ${this.query.conditions.join(' AND ')}`
}
if (this.query.orderBy) {
sql += ` ORDER BY ${this.query.orderBy}`
}
if (this.query.limit) {
sql += ` LIMIT ${this.query.limit}`
}
return sql
}
}
const sql = new QueryBuilder<User>('users')
.where('active = true')
.orderBy('name')
.take(10)
.build()
// SELECT * FROM users WHERE active = true ORDER BY name ASC LIMIT 10
18-4. 型安全なオブジェクトマージ(DeepMerge)
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}
function deepMerge<T extends Record<string, unknown>>(
target: T,
source: DeepPartial<T>
): T {
const result = { ...target }
for (const key of Object.keys(source) as (keyof T)[]) {
const sourceVal = source[key]
const targetVal = target[key]
if (
typeof sourceVal === 'object' &&
sourceVal !== null &&
typeof targetVal === 'object' &&
targetVal !== null
) {
result[key] = deepMerge(
targetVal as Record<string, unknown>,
sourceVal as Record<string, unknown>
) as T[keyof T]
} else if (sourceVal !== undefined) {
result[key] = sourceVal as T[keyof T]
}
}
return result
}
18-5. 型安全なフォームバリデーション(Zodとの組み合わせ)
import { z } from 'zod'
// スキーマから型を導出
const CreateUserSchema = z.object({
name: z.string().min(1, '名前は必須です').max(100),
email: z.string().email('メールアドレスの形式が正しくありません'),
age: z.number().int().min(0).max(120).optional(),
role: z.enum(['admin', 'user', 'guest']).default('user')
})
// Zodスキーマから型を自動生成(手書き不要)
type CreateUserInput = z.infer<typeof CreateUserSchema>
// バリデーション
function validateUser(data: unknown): CreateUserInput {
return CreateUserSchema.parse(data) // 失敗するとZodErrorをthrow
}
// 安全なバリデーション
function safeValidateUser(data: unknown):
| { success: true; data: CreateUserInput }
| { success: false; errors: z.ZodError } {
const result = CreateUserSchema.safeParse(data)
if (result.success) return { success: true, data: result.data }
return { success: false, errors: result.error }
}
18-6. Dependency Injectionパターン
// インターフェイスで依存関係を抽象化
interface Logger {
log(message: string, level?: 'info' | 'warn' | 'error'): void
}
interface EmailService {
send(to: string, subject: string, body: string): Promise<void>
}
interface UserRepository {
findById(id: number): Promise<User | null>
save(user: User): Promise<User>
}
// サービス層(依存はインターフェイスのみ)
class UserService {
constructor(
private readonly userRepo: UserRepository,
private readonly emailService: EmailService,
private readonly logger: Logger
) {}
async createUser(input: CreateUserInput): Promise<User> {
this.logger.log(`Creating user: ${input.email}`)
const user = await this.userRepo.save({
id: Date.now(),
...input,
createdAt: new Date()
} as User)
await this.emailService.send(
user.email,
'ようこそ!',
`${user.name}さん、ご登録ありがとうございます。`
)
return user
}
}
// テスト時はモックを注入
const mockLogger: Logger = { log: jest.fn() }
const mockEmailService: EmailService = { send: jest.fn().mockResolvedValue(undefined) }
const service = new UserService(mockRepo, mockEmailService, mockLogger)
18-7. 型安全なイベントエミッター(実務版)
// イベント名と引数の型を一箇所で管理
interface AppEvents {
'user:signup': { userId: string; email: string; timestamp: Date }
'user:login': { userId: string; ipAddress: string }
'order:placed': { orderId: string; total: number; items: string[] }
'payment:success': { orderId: string; amount: number }
'payment:failed': { orderId: string; reason: string }
}
type EventKey = keyof AppEvents
type EventPayload<K extends EventKey> = AppEvents[K]
type EventListener<K extends EventKey> = (payload: EventPayload<K>) => void | Promise<void>
class AppEventBus {
private listeners = new Map<EventKey, Set<EventListener<EventKey>>>()
on<K extends EventKey>(event: K, listener: EventListener<K>): () => void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set())
}
this.listeners.get(event)!.add(listener as EventListener<EventKey>)
// 購読解除用の関数を返す
return () => this.off(event, listener)
}
off<K extends EventKey>(event: K, listener: EventListener<K>): void {
this.listeners.get(event)?.delete(listener as EventListener<EventKey>)
}
async emit<K extends EventKey>(event: K, payload: EventPayload<K>): Promise<void> {
const listeners = this.listeners.get(event)
if (!listeners) return
await Promise.all([...listeners].map(fn => fn(payload)))
}
}
const bus = new AppEventBus()
// 型安全なイベントの購読と発行
const unsubscribe = bus.on('user:signup', async ({ userId, email }) => {
await sendWelcomeEmail(email) // emailはstring型
})
await bus.emit('user:signup', {
userId: 'u123',
email: 'alice@example.com',
timestamp: new Date()
}) // ✅
await bus.emit('user:signup', { userId: 'u123' }) // ❌ emailとtimestampが足りない
unsubscribe() // 購読解除
18-8. 網羅チェックヘルパー(Exhaustive Switch)
// 再利用可能な網羅チェック関数
export function assertNever(value: never, errorMessage?: string): never {
throw new Error(
errorMessage ?? `網羅されていないケース: ${JSON.stringify(value)}`
)
}
// 使用例
type Notification =
| { type: 'email'; address: string }
| { type: 'sms'; phoneNumber: string }
| { type: 'push'; deviceToken: string }
function sendNotification(notification: Notification): void {
switch (notification.type) {
case 'email':
sendEmail(notification.address)
break
case 'sms':
sendSms(notification.phoneNumber)
break
case 'push':
sendPush(notification.deviceToken)
break
default:
assertNever(notification) // 新しいtypeを追加したらここでエラー
}
}
18-9. 18-X. このセクションのまとめ
実務パターンの使い所:
Branded Types:
→ UserId, PostIdなど「同じ型だが混同してはいけない値」
→ Smart Constructorでバリデーションも一体化
Opaque Types:
→ Branded Typesのより厳密なバージョン
→ unique symbolで型ブランドを作る
Builderパターン:
→ メソッドチェーンで型安全な設定を積み上げる
→ thisを返してチェーンを可能にする
型安全なイベントエミッター:
→ EventMapでイベント名→データ型を管理
→ on/emitの型引数でイベント名を制約
assertNever:
→ switchのdefaultに置いて網羅性を保証
→ 新しいケース追加時のコンパイルエラー安全網
19. 型テスト・型ユーティリティ
このセクションでは型の正確さを確認するための「型テスト」の書き方と、よく使う型ユーティリティの実装を解説します。
19-1. 型テスト(型が正しいことを確認)
// 型チェックを型レベルで書く
type Equals<A, B> =
(<T>() => T extends A ? 1 : 2) extends
(<T>() => T extends B ? 1 : 2) ? true : false
// 使い方(コンパイル時にチェックされる)
type Test1 = Equals<string, string> // true
type Test2 = Equals<string, number> // false
type Test3 = Equals<'a' | 'b', 'b' | 'a'> // true(順序不同)
// Expectヘルパー(falseなら型エラー)
type Expect<T extends true> = T
// 使用例(コンパイル時の型テスト)
type TestGetters = Expect<Equals<
Getters<{ name: string; age: number }>,
{ getName: () => string; getAge: () => number }
>>
// tsdライブラリを使ったより本格的なテスト
import { expectType, expectError } from 'tsd'
expectType<string>(getName(user))
expectError(getUser('not-a-number'))
19-2. 型ユーティリティの実装
// DeepReadonly: 再帰的にreadonly化
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends (infer Item)[]
? ReadonlyArray<DeepReadonly<Item>>
: T[K] extends object
? DeepReadonly<T[K]>
: T[K]
}
// OptionalをRequiredにする
type StrictRequired<T> = {
[K in keyof T]-?: Exclude<T[K], undefined>
}
// オブジェクトの値からユニオン
type ValueOf<T> = T[keyof T]
type Status = { pending: 0; active: 1; inactive: 2 }
type StatusValue = ValueOf<Status> // 0 | 1 | 2
// 関数の引数をPartialに
type PartialParameters<T extends (...args: any[]) => any> =
T extends (...args: infer P) => infer R
? (...args: { [K in keyof P]?: P[K] }) => R
: never
// XOR: AまたはBだが両方ではない
type XOR<A, B> =
| (A & { [K in Exclude<keyof B, keyof A>]?: never })
| (B & { [K in Exclude<keyof A, keyof B>]?: never })
type CreditCard = { cardNumber: string; cvv: string }
type BankTransfer = { bankCode: string; accountNumber: string }
type PaymentMethod = XOR<CreditCard, BankTransfer>
const cc: PaymentMethod = { cardNumber: '1234', cvv: '123' } // ✅
const bt: PaymentMethod = { bankCode: '001', accountNumber: '1234567' } // ✅
const both: PaymentMethod = { cardNumber: '1234', cvv: '123', bankCode: '001', accountNumber: '1234567' } // ❌
19-3. tsd ライブラリを使った本格的な型テスト
ライブラリを開発する場合、型の正確さを自動テストとして保証することが重要です。
// tests/types.test-d.ts(tsdを使った型テスト)
import { expectType, expectError, expectAssignable, expectNotAssignable } from 'tsd'
import { createUser, getUser, updateUser } from '../src/user-service'
import type { User, CreateUserInput } from '../src/types'
// 型が正しく推論されることをテスト
expectType<Promise<User>>(createUser({ name: 'Alice', email: 'alice@example.com' }))
expectType<Promise<User | null>>(getUser(1))
// エラーになることをテスト(型が正しく制限されている)
expectError(getUser('not-a-number')) // numberが必要なので文字列はエラー
expectError(createUser({})) // nameとemailが必要
// 型の代入可能性をテスト
expectAssignable<User>({ id: 1, name: 'Alice', email: 'alice@example.com', createdAt: new Date() })
expectNotAssignable<User>({ id: '1', name: 'Alice' }) // idはnumberが必要
// ジェネリクスの型推論をテスト
import { first, last } from '../src/array-utils'
expectType<string | undefined>(first(['a', 'b', 'c']))
expectType<number | undefined>(last([1, 2, 3]))
// package.jsonのscriptsに追加
// "test:types": "tsd"
vitest / jest での型テスト
// 実行時テストと型テストを組み合わせる
import { describe, it, expectTypeOf } from 'vitest'
import { createStore } from '../src/store'
describe('createStoreの型テスト', () => {
it('stateの型が正しく推論される', () => {
const store = createStore({ count: 0, name: '' })
// expectTypeOfでコンパイル時の型チェック
expectTypeOf(store.getState()).toEqualTypeOf<{ count: number; name: string }>()
expectTypeOf(store.setState).toBeFunction()
expectTypeOf(store.setState).parameter(0).toEqualTypeOf<Partial<{ count: number; name: string }>>()
})
it('不正なsetStateはエラーになる', () => {
const store = createStore({ count: 0 })
expectTypeOf(() => {
// @ts-expect-error: countはnumberなので文字列は不可
store.setState({ count: 'invalid' })
}).toBeFunction()
})
})
19-4. 19-X. このセクションのまとめ
型テスト:
Equals<A, B> → AとBが完全に一致するか
Expect<T extends true> → falseなら型エラー
tsdライブラリ → expectType/expectErrorでより本格的なテスト
便利な型ユーティリティ:
ValueOf<T> → オブジェクトの値型のユニオン
StrictRequired<T> → undefinedも除いて全必須に
PartialParameters<F> → 関数の引数を全オプショナルに
XOR<A, B> → AかBだが両方は許可しない(排他的OR)
20. よくある落とし穴FAQ
このセクションではTypeScript開発者がよく遭遇する問題と解決策をQ&A形式で解説します。
Q1. any を使ってしまう
// ❌ 全てにanyを付ける(型チェックが意味をなさない)
function process(data: any): any {
return data.name
}
// ✅ unknownを使い型チェックを強制
function process(data: unknown): string {
if (typeof data === 'object' && data !== null && 'name' in data) {
return String((data as Record<string, unknown>).name)
}
throw new TypeError('nameプロパティがありません')
}
// ✅ ジェネリクスで型を保持
function identity<T>(value: T): T {
return value
}
Q2. 型アサーション(as)の誤用
// ❌ 危険: 実際の型と異なるものをasで強制
const user = {} as User // コンパイルは通るがランタイムでnullエラー
const el = getElement() as HTMLDivElement // nullかもしれない
// ✅ 正しい型を付ける
const user: User = await fetchUser(id)
// ✅ 型アサーションが本当に必要な場合はガードを加える
const rawEl = document.getElementById('app')
if (rawEl === null) throw new Error('要素が見つかりません')
const el = rawEl as HTMLDivElement // ここではnullが除外されている
Q3. as const を忘れる
// ❌ typeが広くなる
const ROLES = ['admin', 'user', 'guest'] // string[]
type Role = (typeof ROLES)[number] // string(意味がない)
// ✅ as constでリテラル型
const ROLES = ['admin', 'user', 'guest'] as const
type Role = (typeof ROLES)[number] // 'admin' | 'user' | 'guest'
Q4. オプショナルチェイニングと型の関係
// ?. の後はT | undefinedになる
const user = getUser()
const city = user?.address?.city // string | undefined
// これを関数に渡すと...
function process(city: string) {} // stringを期待
process(city) // ❌ Error: string | undefinedはstringに代入できない
// 解決1: nullチェック後に渡す
if (city !== undefined) process(city)
// 解決2: デフォルト値
const displayCity = user?.address?.city ?? '不明'
process(displayCity) // ✅
Q5. 型の循環参照
// ❌ type aliasの単純な循環(コンパイルエラー)
type TreeNode = {
value: number
children: TreeNode[] // 自己参照
}
// ✅ interfaceなら循環参照OK
interface TreeNode {
value: number
children: TreeNode[] // ✅
}
// ✅ typeでもinterfaceを経由すれば可能
type JSONValue = string | number | boolean | null | JSONObject | JSONArray
interface JSONObject { [key: string]: JSONValue }
interface JSONArray extends Array<JSONValue> {}
Q6. ジェネリクスが難しく感じる
ジェネリクスは「型の引数」です。難しく感じるのは「なぜ型を引数にする必要があるか」が見えていないからです。
// なぜTが必要か? → anyを避けて型を保持するため
// ❌ anyを使った場合: 型情報が失われる
function first(arr: any[]): any {
return arr[0]
}
const n = first([1, 2, 3]) // 型: any(numberと知ってほしいのに)
// ✅ ジェネリクスを使った場合: 型が保持される
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
const n = first([1, 2, 3]) // 型: number | undefined(正しい)
const s = first(['a', 'b']) // 型: string | undefined(正しい)
// 段階的な理解:
// 1. まずTをstringに置き換えて読む
// function first(arr: string[]): string | undefined
// 2. それをany型に置き換えてみる(型が失われることを確認)
// 3. Tが「呼び出し時に決まる型」だと理解する
Q7. 型推論が効かない場面
// 型推論が効かない/弱くなる典型的な場面
// 1. 空配列(要素の型が推論できない)
const items = [] // 型: never[](何も追加できない)
const items: string[] = [] // ✅
// 2. 複数の型が返る可能性がある場合
function parse(input: string) {
if (input === 'true') return true // boolean
if (input === '42') return 42 // number
return input // string
}
// 戻り値型: boolean | number | string
// → 呼び出し側で絞り込みが必要になる
// 3. ジェネリクスで型引数が推論できない場合
function create<T>(): T {
return {} as T // Tが決まらない
}
const user = create() // ❌ T = unknown
const user = create<User>() // ✅ 明示的に指定
// 4. オブジェクトの分割代入で型が広がる
const { status } = state
// statusがstringになる場合がある
// (state.statusがstring | 'active' | 'inactive' のとき)
// 5. callbackの返す型がcomplexなとき
const result = arr.reduce((acc, item) => {
// accの型が推論されない場合がある
return { ...acc, [item.key]: item.value }
}, {})
// → initialValueに型注釈を付ける
const result = arr.reduce<Record<string, string>>((acc, item) => {
return { ...acc, [item.key]: item.value }
}, {})
20-X. このセクションのまとめ
よくある落とし穴:
Q1. anyの多用 → unknown + 型ガードで代替
Q2. asの誤用 → 型ガード後に必要な場合のみ使う
Q3. as const忘れ → 定数はas constでリテラル型に
Q4. ?. とundefined → ?. の結果はT | undefined
Q5. 循環参照 → type → interfaceに変えて解決
推論が効かない場面:
空配列: string[] = [] と型注釈を付ける
ジェネリクスの推論失敗: 明示的に <T> を指定
reduceのaccumulator: 初期値に型注釈を付ける
21. 演習問題
このセクションでは学んだ内容を定着させるための演習問題を用意しました。Playground(https://www.typescriptlang.org/play)で試してみてください。
初級
21-A: string | number | null | undefined を受け取り、string を返す toString 関数を型安全に書く(null/undefinedは '' を返す)
21-B: User 型から id と createdAt を除いた型 CreateUserInput をUtility Typesで定義する
21-C: readonly なUserの配列型を作り、要素の追加操作がコンパイルエラーになることを確認する
21-D: 判別可能なユニオン Shape = Circle | Square | Triangle を定義し、面積を求める area(shape: Shape): number を網羅性チェック付きで実装する
中級
21-E: ジェネリックな findBy<T, K extends keyof T>(arr: T[], key: K, value: T[K]): T | undefined 関数を実装する
21-F: 判別可能なユニオン ApiState<T> = Loading | Success<T> | Error を定義し、switch で網羅性チェックが働くことを確認する
21-G: Partial<T> を自前で実装する(type MyPartial<T> = ...)
21-H: DeepPartial<T> を実装する(ネストしたオブジェクトも再帰的にオプショナルに)
21-I: getProperty<T, K extends keyof T>(obj: T, key: K): T[K] を実装し、存在しないキーを渡すとコンパイルエラーになることを確認する
実務
21-J: Zodスキーマから型を導出し、バリデーション関数とともに使う Result<T> パターンを実装する
21-K: EventEmitter クラスをジェネリクスで型付けし、on/off/emit が型安全になるよう実装する
21-L: Branded<T, Brand> 型を実装し、UserId と PostId が混同されないことを確認する
21-M: exhaustiveCheck(value: never): never ヘルパーを実装し、Colorユニオム型のすべてのケースをswitchで処理するコードに適用する
22. 学習ロードマップ(30日)
このセクションではTypeScriptを30日で実務レベルまで習得するためのロードマップを示します。毎日2〜3時間の学習を想定しています。
Week 1: 型システムの基礎
| 日 | 内容 | 目標 |
|---|---|---|
| 1-2 | strict モードで環境構築。プリミティブ型・配列・オブジェクト型 |
tsconfigを設定して最初の型注釈を書ける |
| 3-4 | interface vs type、オプショナル・readonly | 型定義を使い分けられる |
| 5-7 | ユニオン型・インターセクション型・型推論(typeof, keyof) |
ユニオム型を理解してNarrowingができる |
Week 2: 中級機能
| 日 | 内容 | 目標 |
|---|---|---|
| 8-10 | 型のNarrowing(typeof, instanceof, in, 型ガード) | 型ガード関数が書ける |
| 11-12 | ジェネリクスの基本と制約(extends) | <T> を使った汎用関数が書ける |
| 13-14 | ユーティリティ型(Partial, Omit, Pick, Recordなど) | 組み込みユーティリティ型を使いこなせる |
Week 3: 応用
| 日 | 内容 | 目標 |
|---|---|---|
| 15-17 | Mapped Types・Conditional Types・infer | 型変換型が読めて書ける |
| 18-19 | Template Literal Types | 文字列パターンを型で表現できる |
| 20-21 | クラスの型(アクセス修飾子・抽象クラス・implements) | TypeScriptのクラス機能を理解できる |
Week 4: 実務
| 日 | 内容 | 目標 |
|---|---|---|
| 22-24 | エラーハンドリングパターン(unknown, Result型) | 型安全なエラー処理が書ける |
| 25-26 | Zodとの統合、型安全なAPIクライアント | スキーマから型を導出できる |
| 27-30 | 演習問題、実プロジェクトへの適用・tsconfigチューニング | 実務コードにTypeScriptを適用できる |
学習リソース
| リソース | 特徴 |
|---|---|
| TypeScript Handbook | 公式。基礎から応用まで |
| TypeScript Playground | ブラウザで即試せる |
| Total TypeScript | Matt Pocockによる実践的な動画・演習 |
| TypeScript Deep Dive | 無料電子書籍、深い解説 |
| type-challenges | 型パズル集(中〜上級者向け) |
23. 用語集と読み替え表
| 用語 | 一言説明 |
|---|---|
| 型アノテーション | : stringのように明示的に型を書くこと |
| 型推論 | TypeScriptが自動的に型を決定すること |
| 型アサーション | as Type で型を強制的に変換(慎重に使う) |
| 型ガード | value is Type を返す関数でNarrowingを行う |
| Narrowing | 広い型から具体的な型に絞り込む操作 |
| ジェネリクス | 型パラメータを持つ汎用的な型・関数 |
| 構造的型付け | 型の名前でなく構造が一致すれば互換(TypeScriptの基本) |
| Branded Types | 同じプリミティブ型を意味で区別する型テクニック |
| Conditional Types | T extends U ? X : Yの型レベルif/else |
| Mapped Types | 既存の型の全プロパティを変換する型 |
| 宣言マージ | 同名のinterfaceを複数宣言することで型を合成できる機能 |
| never型 | 到達しえない型。網羅性チェックに使う |
| unknown型 | anyより安全。使う前に型チェックが必要 |
| infer | Conditional Types内で型を抽出するキーワード |
| スーパーセット | TypeScriptはJavaScriptの上位互換(すべてのJSコードが有効) |
| トランスパイル | TypeScriptをJavaScriptに変換すること(コンパイルとも言う) |
| 宣言ファイル | .d.ts 拡張子の型定義のみのファイル |
| 分散条件型 | ユニオム型の各メンバーに条件型が自動適用されること |
| 反変 | 引数型において広い型を具体的な型として使える性質 |
| 共変 | 戻り値型においてより具体的な型を広い型として使える性質 |
公式リンク
- TypeScript Handbook
- Total TypeScript - 実践的学習
- TypeScript Deep Dive - 深掘りリソース
24. エコシステム(Zod, tRPC, ts-pattern, type-fest等)
このセクションではTypeScriptと組み合わせることで真価を発揮する主要なエコシステムライブラリを解説します。TypeScriptの型システムを最大限に活用するために、これらのライブラリの役割と使い方を理解することが重要です。
24-1. Zod — ランタイムバリデーション + 型推論
TypeScriptの型はコンパイル時にしか存在しません。APIレスポンスやユーザー入力などのランタイムデータを安全に扱うためにZodを使います。
import { z } from 'zod'
// スキーマ定義(バリデーションルールと型が一体)
const UserSchema = z.object({
id: z.number().int().positive(),
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.coerce.date(), // 文字列をDateに変換
tags: z.array(z.string()).default([])
})
// スキーマから型を自動生成(手書き不要)
type User = z.infer<typeof UserSchema>
// { id: number; name: string; email: string; role: 'admin'|'user'|'guest'; createdAt: Date; tags: string[] }
// parse: 失敗するとZodErrorをthrow
const user = UserSchema.parse(apiResponse)
// safeParse: 失敗を例外でなくResultで返す
const result = UserSchema.safeParse(apiResponse)
if (result.success) {
console.log(result.data.name) // 型: string
} else {
result.error.issues.forEach(issue => {
console.error(`${issue.path.join('.')}: ${issue.message}`)
})
}
// ネストしたスキーマと変換
const CreateUserSchema = UserSchema.omit({ id: true, createdAt: true })
type CreateUserInput = z.infer<typeof CreateUserSchema>
// transform: バリデーション後に変換
const TrimmedStringSchema = z.string().transform(s => s.trim())
const result = TrimmedStringSchema.parse(' hello ') // 'hello'
// superRefine: カスタムバリデーション
const PasswordSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string()
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['confirmPassword'],
message: 'パスワードが一致しません'
})
}
})
Zodを使うべき理由
| 状況 | TypeScriptのみ | TypeScript + Zod |
|---|---|---|
| APIレスポンスの型 | as User(危険) |
UserSchema.parse(data)(安全) |
| フォームバリデーション | 手書きロジック | スキーマから自動 |
| 型とバリデーションの同期 | 手動で一致させる | 常に一致(同じ定義から) |
24-2. tRPC — エンドツーエンドの型安全
tRPCはREST/GraphQLを使わずに、フロントエンドとバックエンドの間で型安全な通信を実現するライブラリです。
// backend: ルーターの定義
import { initTRPC } from '@trpc/server'
import { z } from 'zod'
const t = initTRPC.create()
const appRouter = t.router({
// Queryプロシージャ(GETに相当)
getUser: t.procedure
.input(z.object({ id: z.number() }))
.query(async ({ input }) => {
return db.user.findUnique({ where: { id: input.id } })
// 戻り値の型が自動的に推論される
}),
// Mutationプロシージャ(POST/PUT/DELETEに相当)
createUser: t.procedure
.input(CreateUserSchema)
.mutation(async ({ input }) => {
return db.user.create({ data: input })
}),
})
export type AppRouter = typeof appRouter
// frontend: 型安全な呼び出し
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from '../server/router'
const trpc = createTRPCReact<AppRouter>()
function UserProfile({ id }: { id: number }) {
const { data: user } = trpc.getUser.useQuery({ id })
// userの型: 自動的にサーバーの戻り値型と一致
// user.name, user.emailなど補完が効く
const createUser = trpc.createUser.useMutation()
// createUser.mutate({ name: '...', email: '...' }) で型安全な呼び出し
}
tRPCのメリット: スキーマファースト(Swagger/GraphQLスキーマ)なしでフルスタックの型安全を実現。型の変更が即座にフロントエンドのコンパイルエラーとして現れます。
24-3. ts-pattern — 型安全なパターンマッチング
TypeScriptのswitch/if-elseをより表現力豊かにするライブラリです。
import { match, P } from 'ts-pattern'
type ApiState<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
function renderUser(state: ApiState<User>) {
return match(state)
.with({ status: 'loading' }, () => '<スピナー>')
.with({ status: 'success' }, ({ data }) => `<ユーザー: ${data.name}>`)
.with({ status: 'error' }, ({ error }) => `<エラー: ${error.message}>`)
.exhaustive() // 全ケースを網羅していないとコンパイルエラー
}
// 複雑なパターンマッチング
const result = match({ role: 'admin', plan: 'premium' })
.with({ role: 'admin', plan: 'premium' }, () => 'フル機能')
.with({ role: 'admin' }, () => '管理機能のみ')
.with({ role: 'user', plan: 'premium' }, () => 'プレミアム機能')
.with({ role: 'user' }, () => '基本機能')
.otherwise(() => '不明')
// 型ガードとの組み合わせ
match(value)
.with(P.string, (s) => s.toUpperCase()) // stringに絞り込まれる
.with(P.number, (n) => n.toFixed(2)) // numberに絞り込まれる
.with(P.nullish, () => 'null/undefined') // null | undefined
.otherwise(() => '不明')
24-4. type-fest — 高度な型ユーティリティ集
type-fest はSindre Sorhusが作るTypeScript型ユーティリティのコレクションです。
import type {
Simplify,
SetOptional,
SetRequired,
RequireAtLeastOne,
PartialDeep,
ReadonlyDeep,
JsonValue,
Except,
Merge,
MergeDeep,
Promisable
} from 'type-fest'
// Simplify: 複雑な交差型を展開して読みやすくする
type Complex = { a: string } & { b: number } & { c: boolean }
type Simple = Simplify<Complex>
// { a: string; b: number; c: boolean }
// SetOptional / SetRequired: 特定のキーだけ変更
type User = { id: number; name: string; email: string }
type UserWithOptionalEmail = SetOptional<User, 'email'>
// { id: number; name: string; email?: string }
// RequireAtLeastOne: 少なくとも1つのプロパティを必須に
type SearchParams = RequireAtLeastOne<{
name?: string
email?: string
id?: number
}>
// name, email, idのうち少なくとも1つは必須
// JsonValue: JSONシリアライズ可能な型
function serialize(value: JsonValue): string {
return JSON.stringify(value)
}
serialize({ name: 'Alice', age: 30 }) // ✅
serialize(new Date()) // ❌ DateはJsonValueでない
// Promisable: TまたはPromise<T>
type MaybeAsync<T> = Promisable<T>
async function process(value: MaybeAsync<string>): Promise<string> {
return await value
}
24-5. Prisma — データベースのフルタイプ安全
// schema.prismaでモデルを定義するとTypeScriptの型が自動生成
model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[]
createdAt DateTime @default(now())
}
// 自動生成された型を使ったクエリ(完全型安全)
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// selectで取得するフィールドを指定すると戻り値型も変わる
const user = await prisma.user.findUnique({
where: { id: 1 },
select: { name: true, email: true }
})
// 型: { name: string; email: string } | null(idは含まれない)
// includeでリレーションを含める
const userWithPosts = await prisma.user.findUnique({
where: { id: 1 },
include: { posts: true }
})
// 型: (User & { posts: Post[] }) | null
24-6. 24-X. このセクションのまとめ
TypeScriptエコシステムの役割分担:
Zod:
コンパイル時型 + ランタイムバリデーションを統合
z.infer<typeof Schema> でスキーマから型を導出
APIレスポンス・フォーム・環境変数のバリデーションに使う
tRPC:
フロントエンド↔バックエンドのAPI通信を型安全に
REST/GraphQLスキーマなしでフルスタック型安全
Next.js + tRPCが現代的なフルスタック構成の定番
ts-pattern:
switch/if-elseをパターンマッチングで置き換える
.exhaustive() で網羅性チェックが自動
type-fest:
組み込みにない便利なユーティリティ型集
Simplify, SetOptional, RequireAtLeastOneなど
Prisma:
スキーマからDB操作の型を自動生成
select/includeの結果まで型が変わる高度な型推論
25. TypeScript 5.x新機能
このセクションではTypeScript 5.0〜5.7で追加された重要な新機能を解説します。既存のコードをより安全・簡潔に書けるようになる機能が多く追加されました。
25-1. satisfies 演算子(TS 5.0)
satisfies は「型を検証しつつ、推論された型を保持する」演算子です。as との違いが重要です。
type Color = 'red' | 'green' | 'blue'
type Palette = Record<Color, string | [number, number, number]>
// ❌ asを使った場合: 型チェックはされるが、リテラル型の情報が失われる
const palette = {
red: [255, 0, 0],
green: '#00ff00',
blue: [0, 0, 255]
} as Palette
palette.red // 型: string | [number, number, number](配列と知っているのに)
palette.red.map() // ❌ Error(string | [...] はmapを持たないかもしれない)
// ✅ satisfiesを使った場合: 型チェックしつつリテラル型を保持
const palette = {
red: [255, 0, 0],
green: '#00ff00',
blue: [0, 0, 255]
} satisfies Palette
palette.red // 型: [number, number, number](配列として推論)
palette.red.map() // ✅ 配列のメソッドが使える
palette.green // 型: string(文字列として推論)
palette.green.toUpperCase() // ✅
// 誤った値はsatisfiesでエラーになる
const bad = {
red: 'invalid', // ✅(stringはOK)
green: 42, // ❌ Error: numberはstring | [...] に代入できない
blue: [0, 0] // ❌(要素が2つしかない)
} satisfies Palette
satisfies vs as vs型注釈の使い分け:
| 型チェック | 型の保持(リテラル型) | |
|---|---|---|
| const x: Type = … | ✅(Typeに合わない値はエラー) | ❌(Typeの広い型になる) |
const x = ... as Type |
△(広い型に変換、一部スキップ) | ❌ |
const x = ... satisfies Type |
✅(Typeに合わない値はエラー) | ✅(推論型を保持) |
25-2. const型パラメータ(TS 5.0)
// 従来: 型引数を推論するとリテラル型ではなく広い型になる
function createArray<T>(items: T[]): T[] {
return items
}
const arr = createArray(['hello', 'world'])
// 型: string[]('hello' | 'world' ではなく)
// TS 5.0: const型パラメータで推論をリテラル型に固定
function createArray<const T>(items: T[]): T[] {
return items
}
const arr = createArray(['hello', 'world'])
// 型: ['hello', 'world'](リテラル型のタプルとして推論)
// 実用例: 型安全なルーター
function createRoutes<const T extends readonly string[]>(routes: T): T {
return routes
}
const routes = createRoutes(['/home', '/about', '/users'])
// 型: readonly ['/home', '/about', '/users']
type Route = (typeof routes)[number] // '/home' | '/about' | '/users'
25-3. Variadic Tuple Typesの活用(TS 4.0 〜)
// タプルの展開(スプレッド)
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U]
type AB = Concat<[string, number], [boolean, Date]>
// [string, number, boolean, Date]
// 関数の引数をタプルで操作
type Prepend<T extends unknown[], U> = [U, ...T]
type Append<T extends unknown[], U> = [...T, U]
type Original = [string, number]
type WithId = Prepend<Original, number> // [number, string, number]
type WithDate = Append<Original, Date> // [string, number, Date]
// 実用例: 引数を追加するラッパー型
type WithLogger<T extends (...args: unknown[]) => unknown> =
T extends (...args: infer Args) => infer R
? (...args: [...Args, logger: Logger]) => R
: never
type OrigFn = (a: string, b: number) => boolean
type LoggedFn = WithLogger<OrigFn>
// (a: string, b: number, logger: Logger) => boolean
25-4. using宣言とリソース管理(TS 5.2)
TypeScript 5.2でECMAScriptの using 宣言(Explicit Resource Management)がサポートされました。
// Symbol.disposeを実装したオブジェクトはusingで使える
class DatabaseConnection {
constructor(public readonly url: string) {
console.log(`接続: ${url}`)
}
query(sql: string): unknown[] {
return []
}
[Symbol.dispose]() {
console.log('接続を閉じました')
}
}
// using: スコープを抜けると自動的にdisposeが呼ばれる
function processData() {
using conn = new DatabaseConnection('postgresql://localhost/mydb')
const data = conn.query('SELECT * FROM users')
return data
// スコープを抜けるとconn[Symbol.dispose]() が自動呼び出し
}
// await using: 非同期リソースの管理
class AsyncDatabaseConnection {
async [Symbol.asyncDispose]() {
await this.close()
}
private async close() {
console.log('非同期で接続を閉じました')
}
async query(sql: string): Promise<unknown[]> {
return []
}
}
async function processDataAsync() {
await using conn = new AsyncDatabaseConnection()
const data = await conn.query('SELECT * FROM users')
return data
// スコープを抜けると自動的にawait conn[Symbol.asyncDispose]() が実行
}
25-5. Overrideキーワードの強化(TS 4.3)
class Animal {
move(): void {
console.log('移動')
}
}
class Dog extends Animal {
override move(): void { // overrideキーワードで上書き意図を明示
console.log('走る')
super.move()
}
// overrideを付けていない場合、親クラスにないメソッドを上書きしようとするとエラー
// noImplicitOverride: trueと組み合わせて使う
}
// ❌ 親クラスに存在しないメソッドにoverrideを付けるとエラー
class Cat extends Animal {
override fly(): void { // Error: 'fly' はAnimalに存在しない
console.log('飛べない')
}
}
25-6. Template String Typesの高度な活用(TS 4.1〜)
// 深いネストのオブジェクトパスを型で表現
type DeepPaths<T, Prefix extends string = ''> = {
[K in keyof T & string]:
T[K] extends Record<string, unknown>
? DeepPaths<T[K], `${Prefix}${K}.`> | `${Prefix}${K}`
: `${Prefix}${K}`
}[keyof T & string]
type Config = {
server: { host: string; port: number }
database: { url: string; poolSize: number }
feature: { darkMode: boolean; betaFeatures: string[] }
}
type ConfigPaths = DeepPaths<Config>
// 'server' | 'server.host' | 'server.port' | 'database' | 'database.url' | ...
// 型安全な設定値ゲッター
function getConfig<P extends ConfigPaths>(path: P): unknown {
return path.split('.').reduce((obj, key) => (obj as any)[key], config)
}
getConfig('server.host') // ✅ 型チェックされたパス
getConfig('server.invalid') // ❌ Error: 存在しないパス
25-7. 25-X. このセクションのまとめ
TypeScript 5.xの主要新機能:
satisfies演算子(TS 5.0):
型検証 + 推論型の保持を両立
asとの違い: satisfiesは推論型を保持する
設定オブジェクトや定数の型チェックに最適
const型パラメータ(TS 5.0):
function f<const T>(...) でタプル/リテラル型として推論
ルーター定義や定数配列の型安全に使う
using宣言(TS 5.2):
Symbol.dispose / Symbol.asyncDisposeでリソース管理
DB接続、ファイルハンドル等を自動クリーンアップ
try/finallyの代替として使いやすい
overrideキーワード(TS 4.3):
noImplicitOverrideと組み合わせて上書きの意図を明示
親クラスにないメソッドにoverrideを付けるとエラー
Variadic Tuple Types(TS 4.0〜):
[...T, ...U] でタプルを結合
関数の引数型を型レベルで操作できる
26. 実践:型設計のケーススタディ
このセクションでは実際のアプリケーション開発で遭遇する具体的な型設計の問題と解決策をケーススタディ形式で解説します。「理論はわかったが実際にどう書くのか」という疑問に答えます。
26-1. ケーススタディ: ECサイトのカート機能
// ドメインモデルの型設計
// Branded TypesでIDを安全に
declare const _ProductId: unique symbol
declare const _CartId: unique symbol
declare const _UserId: unique symbol
type ProductId = number & { readonly __brand: typeof _ProductId }
type CartId = string & { readonly __brand: typeof _CartId }
type UserId = string & { readonly __brand: typeof _UserId }
// 商品
interface Product {
readonly id: ProductId
name: string
price: number // 円(整数)
stock: number
category: ProductCategory
}
type ProductCategory = 'electronics' | 'clothing' | 'food' | 'books'
// カートアイテム
interface CartItem {
product: Product
quantity: number
}
// カートの状態(判別可能なユニオン)
type CartState =
| { status: 'empty' }
| { status: 'active'; items: CartItem[]; userId: UserId }
| { status: 'checkout'; items: CartItem[]; userId: UserId; orderId: string }
| { status: 'abandoned'; items: CartItem[]; userId: UserId; abandonedAt: Date }
// カート操作の型
type CartAction =
| { type: 'ADD_ITEM'; product: Product; quantity: number }
| { type: 'REMOVE_ITEM'; productId: ProductId }
| { type: 'UPDATE_QUANTITY'; productId: ProductId; quantity: number }
| { type: 'CLEAR_CART' }
| { type: 'START_CHECKOUT' }
// Reducer(型安全なstate機械)
function cartReducer(state: CartState, action: CartAction): CartState {
switch (state.status) {
case 'empty':
if (action.type === 'ADD_ITEM') {
return {
status: 'active',
items: [{ product: action.product, quantity: action.quantity }],
userId: getCurrentUserId()
}
}
return state
case 'active':
switch (action.type) {
case 'ADD_ITEM': {
const existingIndex = state.items.findIndex(
item => item.product.id === action.product.id
)
if (existingIndex >= 0) {
const newItems = [...state.items]
const existing = newItems[existingIndex]!
newItems[existingIndex] = {
...existing,
quantity: existing.quantity + action.quantity
}
return { ...state, items: newItems }
}
return {
...state,
items: [...state.items, { product: action.product, quantity: action.quantity }]
}
}
case 'REMOVE_ITEM': {
const newItems = state.items.filter(
item => item.product.id !== action.productId
)
if (newItems.length === 0) return { status: 'empty' }
return { ...state, items: newItems }
}
case 'UPDATE_QUANTITY': {
if (action.quantity <= 0) {
// 数量が0以下なら削除と同じ
return cartReducer(state, { type: 'REMOVE_ITEM', productId: action.productId })
}
const newItems = state.items.map(item =>
item.product.id === action.productId
? { ...item, quantity: action.quantity }
: item
)
return { ...state, items: newItems }
}
case 'CLEAR_CART':
return { status: 'empty' }
case 'START_CHECKOUT':
return { ...state, status: 'checkout', orderId: generateOrderId() }
default:
return state
}
case 'checkout':
case 'abandoned':
return state // これらの状態では操作を受け付けない
default:
return assertNever(state)
}
}
// 型安全なカート計算
function calculateTotal(cart: Extract<CartState, { status: 'active' | 'checkout' }>): number {
return cart.items.reduce((sum, item) => sum + item.product.price * item.quantity, 0)
}
// Extractを使ってアクティブなカートのみ受け入れる
function getCartSummary(state: CartState): string {
switch (state.status) {
case 'empty':
return 'カートは空です'
case 'active':
case 'checkout':
return `${state.items.length}点 合計: ¥${calculateTotal(state)}`
case 'abandoned':
return '放置されたカート'
default:
return assertNever(state)
}
}
26-2. ケーススタディ: 型安全な権限システム
// ロールベースアクセス制御(RBAC)の型設計
// リソースとアクションの定義
type Resource = 'user' | 'post' | 'comment' | 'settings'
type Action = 'create' | 'read' | 'update' | 'delete'
// 権限の型(どのリソースにどのアクションができるか)
type Permission = `${Resource}:${Action}`
// 例: 'user:read', 'post:create', 'settings:update' など
// ロールの権限定義
const ROLE_PERMISSIONS = {
guest: ['post:read', 'comment:read'] as const,
user: ['post:read', 'post:create', 'comment:read', 'comment:create', 'user:read'] as const,
moderator: [
'post:read', 'post:create', 'post:update', 'post:delete',
'comment:read', 'comment:create', 'comment:update', 'comment:delete',
'user:read'
] as const,
admin: [
'user:create', 'user:read', 'user:update', 'user:delete',
'post:create', 'post:read', 'post:update', 'post:delete',
'comment:create', 'comment:read', 'comment:update', 'comment:delete',
'settings:read', 'settings:update'
] as const,
} satisfies Record<string, readonly Permission[]>
type Role = keyof typeof ROLE_PERMISSIONS
// ロールから権限のユニオン型を取得
type RolePermissions<R extends Role> = (typeof ROLE_PERMISSIONS)[R][number]
// ユーザーの型
interface AuthUser {
id: UserId
email: string
role: Role
}
// 権限チェック関数(型安全)
function hasPermission<R extends Role>(
user: AuthUser & { role: R },
permission: Permission
): boolean {
const permissions = ROLE_PERMISSIONS[user.role] as readonly Permission[]
return permissions.includes(permission)
}
// 権限をジェネリクスで絞り込む(コンパイル時チェック)
function requirePermission<P extends Permission>(
user: AuthUser,
permission: P
): asserts user is AuthUser & { _hasPermission: P } {
const permissions = ROLE_PERMISSIONS[user.role] as readonly Permission[]
if (!permissions.includes(permission)) {
throw new Error(`権限がありません: ${permission}`)
}
}
// 使用例
async function deletePost(user: AuthUser, postId: number) {
requirePermission(user, 'post:delete')
// この行以降、userは 'post:delete' 権限を持つことが保証される
await db.post.delete({ where: { id: postId } })
}
26-3. ケーススタディ: 型安全な状態管理(Redux-like)
// 型安全なアクションクリエーターとReducer
// アクション型の定義
const createAction = <T extends string, P = void>(type: T) => {
if (undefined === (void 0 as P)) {
// payloadなし
return () => ({ type }) as { type: T }
}
return (payload: P) => ({ type, payload }) as { type: T; payload: P }
}
// アクションの定義
const userActions = {
setUser: (user: User) => ({ type: 'SET_USER' as const, payload: user }),
clearUser: () => ({ type: 'CLEAR_USER' as const }),
updateProfile: (updates: Partial<User>) => ({
type: 'UPDATE_PROFILE' as const,
payload: updates
}),
}
// アクションの型を自動導出
type UserAction = ReturnType<typeof userActions[keyof typeof userActions]>
// Stateの型
interface UserState {
currentUser: User | null
isLoading: boolean
error: string | null
}
const initialUserState: UserState = {
currentUser: null,
isLoading: false,
error: null
}
// 型安全なReducer
function userReducer(state: UserState = initialUserState, action: UserAction): UserState {
switch (action.type) {
case 'SET_USER':
return { ...state, currentUser: action.payload, error: null }
case 'CLEAR_USER':
return { ...state, currentUser: null }
case 'UPDATE_PROFILE':
if (!state.currentUser) return state
return {
...state,
currentUser: { ...state.currentUser, ...action.payload }
}
default:
return assertNever(action)
}
}
26-4. ケーススタディ: 型安全なフォームライブラリ(react-hook-form + Zod)
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
// スキーマ定義(バリデーションルール + 型)
const RegistrationSchema = z.object({
username: z
.string()
.min(3, 'ユーザー名は3文字以上必要です')
.max(20, 'ユーザー名は20文字以下にしてください')
.regex(/^[a-zA-Z0-9_]+$/, '英数字とアンダースコアのみ使用可能です'),
email: z.string().email('メールアドレスの形式が正しくありません'),
password: z
.string()
.min(8, 'パスワードは8文字以上必要です')
.regex(/[A-Z]/, '大文字を含めてください')
.regex(/[0-9]/, '数字を含めてください'),
confirmPassword: z.string(),
acceptTerms: z.literal(true, {
errorMap: () => ({ message: '利用規約への同意が必要です' })
}),
}).refine(data => data.password === data.confirmPassword, {
message: 'パスワードが一致しません',
path: ['confirmPassword']
})
type RegistrationFormValues = z.infer<typeof RegistrationSchema>
// React Hook Formとの統合
function RegistrationForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<RegistrationFormValues>({
resolver: zodResolver(RegistrationSchema)
})
const onSubmit = async (data: RegistrationFormValues) => {
// dataは型が保証されている
await registerUser({
username: data.username,
email: data.email,
password: data.password
})
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
{...register('username')}
placeholder="ユーザー名"
/>
{errors.username && <span>{errors.username.message}</span>}
<input
type="email"
{...register('email')}
placeholder="メールアドレス"
/>
{errors.email && <span>{errors.email.message}</span>}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '登録中...' : '登録'}
</button>
</form>
)
}
26-5. ケーススタディ: 型安全な国際化(i18n)
// 翻訳キーを型で管理
// 翻訳辞書の型(ネストしたオブジェクト)
const ja = {
common: {
loading: '読み込み中...',
error: 'エラーが発生しました',
retry: '再試行',
cancel: 'キャンセル',
save: '保存'
},
user: {
profile: 'プロフィール',
settings: '設定',
logout: 'ログアウト',
greeting: (name: string) => `こんにちは、${name}さん`
},
errors: {
notFound: (resource: string) => `${resource}が見つかりません`,
unauthorized: '権限がありません',
networkError: 'ネットワークエラーが発生しました'
}
} as const
type Translations = typeof ja
// ドット区切りのパスを型で表現
type DotPaths<T, Prefix extends string = ''> = {
[K in keyof T & string]:
T[K] extends (...args: never[]) => string
? `${Prefix}${K}` // 関数はそのままパスとして
: T[K] extends string
? `${Prefix}${K}` // 文字列もパスとして
: DotPaths<T[K], `${Prefix}${K}.`> // ネストは再帰
}[keyof T & string]
type TranslationKey = DotPaths<Translations>
// 'common.loading' | 'common.error' | 'user.profile' | 'errors.notFound' | ...
// 型安全なt関数
function t(key: TranslationKey, ...args: string[]): string {
const parts = key.split('.')
let current: unknown = ja
for (const part of parts) {
current = (current as Record<string, unknown>)[part]
}
if (typeof current === 'function') {
return (current as (...args: string[]) => string)(...args)
}
return current as string
}
// 使用例
t('common.loading') // ✅ '読み込み中...'
t('user.greeting', 'Alice') // ✅ 'こんにちは、Aliceさん'
t('errors.notFound', 'ユーザー') // ✅ 'ユーザーが見つかりません'
t('common.invalid') // ❌ Error: 存在しないキー
26-6. 型安全な設定(Config)管理パターン
// 環境変数を型安全に扱う
import { z } from 'zod'
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']),
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().int().min(1).max(65535).default(3000),
JWT_SECRET: z.string().min(32, 'JWTシークレットは32文字以上必要です'),
REDIS_URL: z.string().url().optional(),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
MAX_UPLOAD_SIZE_MB: z.coerce.number().positive().default(10),
})
// 型を自動導出
type Env = z.infer<typeof EnvSchema>
// 起動時に検証(失敗すると起動エラー)
function loadEnv(): Env {
const result = EnvSchema.safeParse(process.env)
if (!result.success) {
console.error('環境変数の設定が不正です:')
result.error.issues.forEach(issue => {
console.error(` ${issue.path.join('.')}: ${issue.message}`)
})
process.exit(1)
}
return result.data
}
// シングルトンとしてexport
export const env = loadEnv()
// 使う側: 型が保証されている
console.log(env.PORT) // number
console.log(env.DATABASE_URL) // string(URL形式)
console.log(env.NODE_ENV) // 'development' | 'test' | 'production'
26-7. 型安全なAPIルーティング
// Next.js App Router風の型安全ルーター
// ルートパラメータを型で定義
type RouteParams = {
'/users': {}
'/users/:id': { id: string }
'/posts': {}
'/posts/:id': { id: string }
'/posts/:id/comments': { id: string }
'/posts/:id/comments/:commentId': { id: string; commentId: string }
}
type Route = keyof RouteParams
// パラメータありのルートとなしのルートを分離
type ParamlessRoutes = {
[K in Route]: RouteParams[K] extends Record<string, never> ? K : never
}[Route]
type ParamRoutes = Exclude<Route, ParamlessRoutes>
// 型安全なURL生成
function buildUrl(route: ParamlessRoutes): string
function buildUrl<R extends ParamRoutes>(route: R, params: RouteParams[R]): string
function buildUrl(route: Route, params?: Record<string, string>): string {
if (!params) return route
return Object.entries(params).reduce(
(url, [key, value]) => url.replace(`:${key}`, value),
route as string
)
}
// 使用例
const usersUrl = buildUrl('/users') // ✅
const userUrl = buildUrl('/users/:id', { id: '123' }) // ✅
const commentUrl = buildUrl(
'/posts/:id/comments/:commentId',
{ id: '1', commentId: '42' }
) // ✅
buildUrl('/users/:id') // ❌ Error: paramsが必要
buildUrl('/users/:id', { userId: '123' }) // ❌ Error: 'id' が必要なのに 'userId'
26-8. 26-X. このセクションのまとめ
実践的な型設計の原則:
1. ドメインモデルにBranded Typesを使う
UserId, ProductId, CartIdなど混同を防ぐ
2. 状態は判別可能なユニオンで表現する
{ status: 'empty' | 'active' | 'checkout' } 形式
switch + exhaustiveCheckで安全な状態遷移
3. 権限は型で表現する
Permission = `${Resource}:${Action}` のパターン
requirePermissionでアサーション関数
4. フォームバリデーションはZod + react-hook-form
スキーマから型を導出(手書き不要)
ランタイムバリデーションと型が常に一致
5. 環境変数は起動時にZodでバリデーション
失敗したら即座にエラー終了(runtimeで型安全)
6. 設定やルートはsatisfiesで型チェック + 推論保持
27. パフォーマンスとスケーラビリティ
このセクションでは大規模TypeScriptプロジェクトでの型チェックパフォーマンスと、スケールするコードベースを維持するための実践的な知識を解説します。
27-1. 型チェックが遅くなる原因と対策
// ❌ 深い再帰型は型チェックが遅くなる
type DeepNested<T, Depth extends number = 10> = {
value: T
children: Depth extends 0 ? never : DeepNested<T, ...> // 深い再帰
}
// ✅ 深さを制限する
type DepthCount = [never, 0, 1, 2, 3, 4, 5]
type DeepNested<T, D extends number = 5> = {
value: T
children: D extends 0 ? never : DeepNested<T, DepthCount[D]>[]
}
// ❌ 巨大なユニオン型は型チェックが指数的に遅くなる
type AllCombinations<T extends string, U extends string> =
`${T}-${U}` | `${U}-${T}`
// TとUが大きいと組み合わせ爆発
// ✅ 必要な組み合わせのみを定義
type ValidCombination = 'read-user' | 'write-user' | 'read-post' | 'write-post'
27-2. skipLibCheck と型チェックの境界
{
"compilerOptions": {
// skipLibCheck: node_modulesの .d.tsファイルの型チェックをスキップ
// メリット: 型チェック速度が大幅向上
// デメリット: ライブラリの型の問題に気づきにくい
"skipLibCheck": true, // ほとんどのプロジェクトで推奨
// incremental: 差分コンパイルで速度向上
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo"
}
}
27-3. モジュール境界での型の公開と隠蔽
// 内部実装の型はexportしない
// 公開APIの型のみexportする
// ✅ 公開する型
export type { User, CreateUserInput, UserUpdateInput } from './types'
export type { UserService } from './services/user-service'
// ❌ 内部実装の型を漏らさない
// export type { InternalUserCache } from './cache' // 内部詳細は非公開
// index.tsでバレルエクスポートを管理
// packages/user/index.ts
export type { User, CreateUserInput } from './types'
export { UserService } from './services/UserService'
// 内部実装はexportしない
27-4. 型エラーへの対処法(段階的な型強化)
既存のJavaScriptプロジェクトをTypeScriptに移行する際の戦略:
// フェーズ1: allowJsでJSファイルをそのまま取り込む
// tsconfig.json
{
"compilerOptions": {
"allowJs": true, // .jsファイルをTypeScriptプロジェクトに含める
"checkJs": false, // JSファイルの型チェックはOFFのまま
"strict": false // 最初はゆるく始める
}
}
// フェーズ2: .js → .tsへ段階的にリネーム
// 一度に全部変換せず、モジュールごとに移行
// フェーズ3: anyをunknownに、型ガードを追加
// ❌ 移行初期のコード
function processData(data: any) { // anyで妥協
return data.name
}
// ✅ 型ガードを追加して安全化
function processData(data: unknown) {
if (typeof data === 'object' && data !== null && 'name' in data) {
return String((data as Record<string, unknown>).name)
}
throw new TypeError('nameプロパティがありません')
}
// フェーズ4: strict: trueを有効化
// エラーを一つずつ修正
// // @ts-ignoreと // @ts-expect-errorの使い分け
// @ts-ignore: エラーを無視する(なぜ無視するか説明が必要)
// // @ts-ignore: この行はJSライブラリの型定義が不完全なため
const result = legacyLib.doSomething(value)
// @ts-expect-error: エラーが出ることを期待する(テスト用)
// @ts-expect-error
getUser('invalid-type') // 意図的にエラーになる呼び出し
27-5. 27-X. このセクションのまとめ
型チェックパフォーマンス:
深い再帰型は遅い → 深さを制限する
巨大なユニオン型は遅い → 必要なものだけ定義
skipLibCheck: trueでnode_modulesをスキップ
incremental: trueで差分コンパイル
モジュール境界:
公開APIの型のみexport
内部実装はexportしない
index.tsでバレルエクスポートを管理
段階的移行:
allowJs → .jsを .tsにリネーム → 型ガード追加 → strict: true
// @ts-ignoreは理由を必ずコメントで書く
// @ts-expect-errorはテストコードで使う
28. デバッグと型エラーの読み方
このセクションではTypeScriptの型エラーメッセージを正確に読み解く方法と、型の問題をデバッグするテクニックを解説します。型エラーメッセージは最初は難解に見えますが、読み方を知れば素早く問題を特定できます。
28-1. よくある型エラーの読み方
// エラー1: "Type 'X' is not assignable to type 'Y'"
// 意味: XをYとして使おうとしたが、XはYの条件を満たさない
const x: string = 42
// Error: Type 'number' is not assignable to type 'string'
// 読み方: number (42の型) をstringとして使おうとしているが、互換性がない
// エラー2: "Property 'X' does not exist on type 'Y'"
// 意味: Y型のオブジェクトにXというプロパティが存在しない
const user: User = { id: 1, name: 'Alice' }
user.email
// Error: Property 'email' does not exist on type 'User'
// 読み方: User型に 'email' プロパティがない(typoかもしれない)
// エラー3: "Argument of type 'X' is not assignable to parameter of type 'Y'"
// 意味: 関数の引数としてXを渡そうとしたが、Yが期待されている
function greet(name: string) {}
greet(42)
// Error: Argument of type 'number' is not assignable to parameter of type 'string'
// エラー4: "Object is possibly 'null'"
// 意味: この値がnullの可能性があるので操作できない
const el = document.getElementById('app')
el.style.display = 'none'
// Error: Object is possibly 'null'
// 解決: nullチェックを追加する
if (el !== null) {
el.style.display = 'none'
}
28-2. 型のデバッグテクニック
// テクニック1: typeを使って中間の型を確認
type TestType = ReturnType<typeof complexFunction>
// VSCodeでホバーすると型が表示される
// テクニック2: 変数に型注釈を付けてエラーを確認
const result = complexOperation() // 型が不明
const result2: ExpectedType = complexOperation() // 型が合わないとエラー
// テクニック3: satisfiesで型を検証
const config = {
host: 'localhost',
port: 3000
} satisfies Config // Configに合わない場合エラー
// テクニック4: Playgroundで最小限のコードを再現
// https://www.typescriptlang.org/playに貼り付けて確認
// テクニック5: tsdライブラリでテストを書く
import { expectType, expectError } from 'tsd'
expectType<string>(getName(user)) // stringと推論されるはず
expectError(getUser('not-a-number')) // エラーになるはず
// テクニック6: conditional typeで型を検査
type IsString<T> = T extends string ? 'yes' : 'no'
type Check = IsString<ReturnType<typeof someFunction>> // 'yes' か 'no' か確認
28-3. TypeScriptのPlaygroundを活用する
TypeScript Playground(https://www.typescriptlang.org/play)は最も便利なデバッグツールです。
活用方法:
- 型エラーが出たコードを最小限に絞ってPlaygroundに貼り付ける
.d.tsタブで推論された型定義を確認する- 「Options」でコンパイラオプションを切り替えて動作を確認する
- 「Share」でリンクを作成してチームに共有する
28-4. 28-X. このセクションのまとめ
よくある型エラーのパターン:
"not assignable to type" → 型の互換性がない
"does not exist on type" → プロパティ名が違う(typo)
"Object is possibly null" → nullチェックが必要
"Parameter X implicitly has an 'any' type" → 引数に型注釈が必要
デバッグテクニック:
type TestType = ... でホバー確認
satisfiesで型を検証
Playgroundで最小限のコードを再現
tsdで型のテストを書く
効果的なエラー修正の順序:
1. エラーメッセージを読む(上から1つずつ)
2. エラーが出た行を特定する
3. 期待される型と実際の型を確認する
4. 型ガードや型変換で修正する
29. TypeScriptとJavaScriptの相互運用
このセクションではTypeScriptとJavaScriptを混在させる実務的なシナリオを解説します。既存のJavaScriptライブラリを使う場合や、段階的移行中のプロジェクトで重要な知識です。
29-1. JSDocを使ったJavaScriptの型付け
TypeScriptに移行する前段として、JavaScriptファイルにJSDocで型を付けることができます。
// @ts-checkを先頭に書くとJSDocから型チェックが有効になる
// @ts-check
/**
* @param {string} name - ユーザー名
* @param {number} age - 年齢
* @returns {{ name: string; age: number; greeting: string }}
*/
function createUser(name, age) {
return {
name,
age,
greeting: `こんにちは、${name}さん`
}
}
/**
* @template T
* @param {T[]} arr - 配列
* @returns {T | undefined}
*/
function first(arr) {
return arr[0]
}
// 型のインポート(TypeScriptの型定義から)
/**
* @param {import('./types').User} user
* @returns {Promise<void>}
*/
async function sendEmail(user) {
await emailService.send(user.email, 'こんにちは')
}
29-2. 型定義のないnpmパッケージを使う
// ケース1: @types/xxxパッケージがある場合
// npm install -D @types/lodash
import _ from 'lodash'
_.chunk([1, 2, 3, 4], 2) // 型安全に使える
// ケース2: @types/xxxがない場合(自前で型を書く)
// types/some-lib.d.ts
declare module 'some-untyped-lib' {
export interface Config {
timeout: number
retries?: number
onError?: (error: Error) => void
}
export function initialize(config: Config): void
export function doOperation(input: string): Promise<string>
export const version: string
}
// 使う側
import { initialize, doOperation } from 'some-untyped-lib'
initialize({ timeout: 5000 }) // 型安全
// ケース3: 最も簡単な方法(型をanyにする)
// types/quick-declare.d.ts
declare module 'another-untyped-lib'
// importするとany型になる(最後の手段)
29-3. allowJsとcheckJsの使い分け
{
"compilerOptions": {
// allowJs: true → .jsファイルをTypeScriptプロジェクトに含める
// checkJs: false → JSファイルの型チェックはしない(デフォルト)
"allowJs": true,
"checkJs": false,
// checkJs: trueにするとJSDocの型チェックが有効になる
// allowJs: trueとcheckJs: trueを両方trueにすると
// .jsファイルでも型チェックが効く(段階的移行に便利)
"checkJs": true
}
}
29-4. 29-X. このセクションのまとめ
JavaScriptとの共存:
allowJs: trueで .jsをTypeScriptプロジェクトに含める
checkJs: trueでJSDocから型チェックを有効化
型定義のないパッケージへの対応:
@types/xxxを探す(DefinitelyTyped)
なければdeclare moduleで自前の型を書く
最後の手段: declare module 'name' のみ(any型)
JSDocでの型付け:
@ts-checkでTypeScriptの型チェックをJSファイルに適用
@param, @returns, @templateで型を指定
TypeScriptへの移行前の段階として有効
付録A: TypeScriptチートシート
よく使う型と構文のクイックリファレンスです。
A-1. 型の宣言クイックリファレンス
// ─── プリミティブ型 ───────────────────────────────────
const n: number = 42
const s: string = 'hello'
const b: boolean = true
const nil: null = null
const undef: undefined = undefined
// ─── 特殊型 ──────────────────────────────────────────
const a: any = 'anything goes' // 型チェック無効(避ける)
const u: unknown = 'check before use' // 安全なany(使用前にチェック必要)
function never(): never { throw new Error() } // 到達しない
function void_fn(): void { console.log() } // 戻り値なし
// ─── オブジェクト型 ───────────────────────────────────
interface User { id: number; name: string }
type Point = { x: number; y: number }
// ─── 配列 ─────────────────────────────────────────────
const arr1: number[] = [1, 2, 3]
const arr2: Array<string> = ['a', 'b']
const readonly1: readonly number[] = [1, 2, 3]
const readonly2: ReadonlyArray<string> = ['a', 'b']
// ─── タプル ───────────────────────────────────────────
const tuple: [string, number] = ['hello', 42]
const namedTuple: [name: string, age: number] = ['Alice', 30]
// ─── ユニオム・インターセクション ─────────────────────
type StringOrNumber = string | number
type Named = { name: string }
type Aged = { age: number }
type Person = Named & Aged
// ─── リテラル型 ──────────────────────────────────────
type Status = 'pending' | 'active' | 'inactive'
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6
// ─── ジェネリクス ─────────────────────────────────────
function identity<T>(value: T): T { return value }
type Box<T> = { value: T }
type Pair<A, B> = { first: A; second: B }
// ─── ユーティリティ型 ────────────────────────────────
type PartialUser = Partial<User> // 全オプショナル
type RequiredUser = Required<User> // 全必須
type ReadonlyUser = Readonly<User> // 全readonly
type PublicUser = Pick<User, 'id' | 'name'> // 特定のみ
type UserWithoutId = Omit<User, 'id'> // 特定を除く
type UserMap = Record<string, User> // キー→値
type Fn = ReturnType<typeof identity> // 関数の戻り値型
type Args = Parameters<typeof identity> // 関数の引数型(タプル)
type Resolved = Awaited<Promise<User>> // Promise解決型
// ─── 修飾子 ───────────────────────────────────────────
type WithOptional = { name?: string } // オプショナル
type WithReadonly = { readonly id: number } // 読み取り専用
type WithIndex = { [key: string]: unknown } // インデックスシグネチャ
// ─── 型の取得 ─────────────────────────────────────────
const user = { id: 1, name: 'Alice' }
type UserType = typeof user // 値から型を取得
type UserKeys = keyof typeof user // 'id' | 'name'
type IdType = (typeof user)['id'] // number(インデックスアクセス)
A-2. Narrowingクイックリファレンス
function process(value: string | number | null | User) {
// typeof
if (typeof value === 'string') { value.toUpperCase() }
if (typeof value === 'number') { value.toFixed(2) }
// null/undefinedチェック
if (value === null) { /* null */ }
if (value != null) { /* nullでもundefinedでもない */ }
// instanceof
if (value instanceof Error) { value.message }
// in演算子
if ('name' in value) { value.name } // User型に絞り込まれる
// 型ガード関数
function isUser(v: unknown): v is User {
return typeof v === 'object' && v !== null && 'id' in v
}
if (isUser(value)) { value.id }
// アサーション関数
function assertString(v: unknown): asserts v is string {
if (typeof v !== 'string') throw new TypeError()
}
assertString(value)
value.toUpperCase() // string確定
}
A-3. ジェネリクスクイックリファレンス
// 基本
function id<T>(v: T): T { return v }
// 制約
function len<T extends { length: number }>(v: T): number { return v.length }
// 複数の型変数
function map<T, U>(arr: T[], fn: (x: T) => U): U[] { return arr.map(fn) }
// デフォルト型パラメータ
interface Repo<T, ID = number> { findById(id: ID): T | null }
// 条件型
type IsArray<T> = T extends unknown[] ? true : false
// infer
type ElementOf<T> = T extends (infer E)[] ? E : never
type ReturnOf<T> = T extends (...args: never[]) => infer R ? R : never
// Mapped Types
type Optional<T> = { [K in keyof T]?: T[K] }
type Nullable<T> = { [K in keyof T]: T[K] | null }
type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }
A-4. tsconfig主要フラグ早見表
| フラグ | 推奨値 | 効果 |
|---|---|---|
strict |
true |
厳格チェック一括有効 |
strictNullChecks |
true(strictに含む) |
null/undefinedを分離 |
noImplicitAny |
true(strictに含む) |
暗黙的anyを禁止 |
noUncheckedIndexedAccess |
true |
arr[i] にundefinedを追加 |
exactOptionalPropertyTypes |
true |
? とundefinedを区別 |
noImplicitReturns |
true |
全パスにreturnを強制 |
noFallthroughCasesInSwitch |
true |
switchフォールスルー禁止 |
skipLibCheck |
true |
node_modulesの型チェックをスキップ |
incremental |
true |
差分コンパイルで高速化 |
declaration |
ライブラリ公開時 | .d.tsファイルを生成 |
sourceMap |
true |
デバッグ用ソースマップ |
noEmit |
バンドラ使用時 | ファイル生成なし(型チェックのみ) |
A-5. よく使うコマンド早見表
# インストール
npm install -D typescript
pnpm add -D typescript
# tsconfig生成
npx tsc --init
# 型チェックのみ(ファイル生成なし)
npx tsc --noEmit
# ウォッチモード
npx tsc --watch --noEmit
# プロジェクト参照のビルド
npx tsc --build
# 特定ファイルのコンパイル
npx tsc src/index.ts
# バージョン確認
npx tsc --version
# 型定義のインストール
npm install -D @types/node
npm install -D @types/react
# 型のないnpmパッケージを確認
# https://www.npmjs.com/package/@types/<package-name>
30. まとめ
このセクションでは本ガイドで学んだ内容を振り返り、TypeScriptをさらに深く学ぶためのロードマップを示します。
30-1. 本ガイドで学んだこと
基礎(セクション1〜5):
✅ TypeScriptが生まれた背景と設計思想
✅ tsconfigの各オプションの意味
✅ プリミティブ型・any/unknown/neverの違い
✅ 型推論の仕組みと文脈型付け
✅ interface vs type aliasの使い分け
中級(セクション6〜12):
✅ ユニオン型・判別可能なユニオン・網羅性チェック
✅ Narrowingの全手法(typeof, instanceof, in, 型ガード)
✅ ジェネリクス・infer・共変・反変
✅ 組み込みユーティリティ型と自前実装
✅ Mapped Types・Conditional Types・Template Literal Types
✅ 関数オーバーロード・型の互換性
✅ strictモードの全フラグ
応用(セクション13〜20):
✅ クラスのアクセス修飾子・抽象クラス
✅ モジュール・宣言ファイル・型のインポート
✅ 非同期処理の型・型安全なイベントエミッター
✅ Result型・エラーハンドリングパターン
✅ Branded Types・DIパターン・実務パターン集
実践(セクション21〜30):
✅ エコシステム(Zod, tRPC, ts-pattern, type-fest)
✅ TypeScript 5.x新機能(satisfies, const type parameter)
✅ ケーススタディ(ECサイト・権限システム・状態管理)
✅ 型チェックパフォーマンス・段階的移行
✅ 型エラーのデバッグテクニック
30-2. 次のステップ
初級から中級へ:
- TypeScript Playgroundで毎日1つ型パズルを解く
type-challenges(GitHub)で演習問題に取り組む- 既存のJavaScriptコードをTypeScriptに移行してみる
中級から上級へ:
- 自分のプロジェクトでstrict: true + noUncheckedIndexedAccess: trueを有効化
- ジェネリクスと
inferを使って便利なユーティリティ型を作る - Zod + tRPCでフルスタックの型安全なアプリを作る
上級からTypeScriptエキスパートへ:
- TypeScriptのコンパイラオプションを深く理解する
- 型レベルのプログラミング(Template Literal Types + inferの組み合わせ)
- TypeScriptのソースコードを読む(型チェッカーの動作を理解する)
30-3. コミュニティリソース
| リソース | 内容 | レベル |
|---|---|---|
| TypeScript Handbook | 公式ドキュメント | 初〜中級 |
| Total TypeScript | Matt Pocockの実践的コース | 中〜上級 |
| type-challenges | 型パズル集 | 中〜上級 |
| TypeScript Deep Dive | 深い解説の無料電子書籍 | 中〜上級 |
| ts-pattern | パターンマッチングライブラリ | 中級 |
| type-fest | ユーティリティ型集 | 中〜上級 |
| Zod | ランタイムバリデーション + 型 | 中級 |
| tRPC | フルスタック型安全API | 中〜上級 |
30-4. 30-X. このガイドのまとめ
TypeScriptの核心:
型はコンパイル時にのみ存在する(ランタイムに消える)
構造的型付け: 型の名前でなく構造が一致すれば互換
段階的移行: 既存JSに徐々に型を追加できる
型システムの強力な機能:
ジェネリクス → anyを使わずに汎用的なコードを書く
Narrowing → 制御フローで型を自動絞り込み
Mapped Types → 型変換を宣言的に定義
infer → 型の一部を「取り出す」
satisfies → 型チェックしつつ推論型を保持
実務でよく使うパターン:
Branded Types → IDの混同を防ぐ
判別可能なユニオン → 状態を型で表現
Result<T> → エラーを型で表現
型ガード関数 → value is TypeでNarrowing
assertNever → switchの網羅性チェック
エコシステムの活用:
Zod → ランタイムバリデーション + 型の一元管理
tRPC → フロントエンド↔バックエンドの型安全通信
Prisma → データベーススキーマから型を自動生成
まとめ
TypeScriptは、JavaScriptに静的型付けを重ねることで、設計意図、API境界、リファクタリングの安全性を高める言語です。型を細かく書くこと自体が目的ではなく、変更に強い境界を作り、実行時のJavaScriptとのずれを理解しながら使うことが大切です。
参考文献
公式・標準
書籍
解説・補助
- tRPC - End-to-End Type Safety
- Zod - ランタイムバリデーション + 型推論
- ts-pattern - パターンマッチング
- type-challenges - 型パズル集
- type-fest - ユーティリティ型コレクション
- TypeScript GitHub Repository
- Total TypeScript - 実践的学習(Matt Pocock)