【実践】Zod の union・discriminatedUnion を使った柔軟な型定義

TypeScriptの開発において、複数の型の可能性を持つデータを扱うことは日常茶飯事です。APIレスポンスが成功時と失敗時で異なる形式を持ったり、フォームの入力内容によって必要な項目が変わったりと、柔軟な型定義が求められる場面は数多く存在します。
そんな時に頼りになるのが、Zodのunion()
とdiscriminatedUnion()
という2つの強力な機能です。これらを使いこなすことで、実行時の型検証と静的型チェックの両方を満たす、堅牢で柔軟なアプリケーションを構築できるでしょう。
本記事では、基本的な使い方から実際の開発現場で活用できる応用例まで、段階的に解説していきます。コードを書きながら学べるよう、豊富な実例とともにお届けしますので、ぜひ最後までお付き合いください。
背景
TypeScriptの型システムとZodの関係
TypeScriptは静的型チェックによって開発時の安全性を高めてくれますが、実行時のデータ検証までは行えません。外部APIから取得したデータや、ユーザーからの入力データが期待する型と一致するかは、実行時に確認する必要があります。
ここでZodが威力を発揮します。Zodはスキーマ定義から TypeScript の型を自動生成し、同時に実行時のバリデーションも提供する優れたライブラリです。
以下の図で、TypeScriptとZodがどのように連携するかを確認しましょう。
mermaidflowchart TB
schema[Zodスキーマ定義] -->|型生成| ts_type[TypeScript型]
schema -->|実行時検証| validation[バリデーション]
external_data[外部データ] -->|検証| validation
validation -->|成功| safe_data[型安全なデータ]
validation -->|失敗| error[バリデーションエラー]
safe_data --> app[アプリケーション]
ts_type --> app
この連携により、開発時の型安全性と実行時のデータ検証を一つのスキーマで実現できるのです。
union型が必要になる実際のユースケース
実際の開発では、単一の型では表現できない複雑なデータ構造に遭遇することがよくあります。以下のような場面でunion型の威力を実感できるでしょう。
# | ユースケース | 説明 |
---|---|---|
1 | APIレスポンスの状態管理 | 成功・失敗・ローディング状態で異なる構造 |
2 | フォームの条件分岐 | 入力タイプによって必要な項目が変化 |
3 | 設定データの環境別管理 | 開発・本番環境で異なる設定値 |
4 | イベント処理の多様性 | イベントタイプごとに異なるペイロード |
例えば、ECサイトの商品データを考えてみましょう。物理商品とデジタル商品では、必要な情報が大きく異なります。
typescript// 物理商品の場合
interface PhysicalProduct {
type: 'physical'
name: string
price: number
weight: number
shippingCost: number
}
// デジタル商品の場合
interface DigitalProduct {
type: 'digital'
name: string
price: number
downloadUrl: string
fileSize: number
}
このような場合に、union型を使って柔軟に型定義を行うことで、コードの可読性と安全性を両立できます。
課題
通常のunion型の限界
TypeScriptの標準的なunion型だけでは、いくつかの課題に直面することがあります。特に実行時の型判定において、思わぬ落とし穴が待っているのです。
まず、基本的なunion型の問題を見てみましょう。
typescript// 基本的なunion型の定義
type Product = PhysicalProduct | DigitalProduct
// 実行時の型判定が複雑
function processProduct(product: Product) {
// どちらの型か判定するのが困難
if ('weight' in product) {
// PhysicalProductと推論される
console.log(`重量: ${product.weight}kg`)
} else {
// DigitalProductと推論される
console.log(`ファイルサイズ: ${product.fileSize}MB`)
}
}
この方法では、プロパティの存在確認による型の絞り込みに頼らざるを得ません。
型判定の複雑さと実行時エラーの課題
実際の開発では、以下のような問題が頻繁に発生します。
typescript// 外部APIからのデータ(型が不明)
const apiResponse: unknown = {
type: 'physical',
name: 'ノートパソコン',
price: 80000,
weight: 2.5,
// shippingCostが欠落している可能性
}
// 型アサーションは危険
const product = apiResponse as Product
// 実行時エラーの可能性が高い
console.log(product.shippingCost) // undefined になる可能性
以下の図で、従来の型判定手法における問題点を整理します。
mermaidflowchart TD
unknown_data[外部データ] --> type_guard{型ガード}
type_guard -->|プロパティ存在チェック| complex_logic[複雑な判定ロジック]
type_guard -->|型アサーション| unsafe_cast[危険な型変換]
complex_logic --> maintenance_cost[保守性の低下]
complex_logic --> bug_risk[バグの温床]
unsafe_cast --> runtime_error[実行時エラー]
unsafe_cast --> type_mismatch[型不整合]
maintenance_cost --> problem[開発効率の低下]
bug_risk --> problem
runtime_error --> problem
type_mismatch --> problem
従来の手法では、型安全性と開発効率のトレードオフに悩まされることが多かったのです。
図で理解できる要点
- プロパティ存在チェックによる型判定は複雑で保守性が低い
- 型アサーションは危険で実行時エラーの原因となる
- 結果として開発効率と安全性の両方が犠牲になってしまう
解決策
これらの課題を解決するために、Zodは2つの強力な機能を提供しています。まずは基本的なunion()
から見ていきましょう。
union()の基本的な使い方
Zodのunion()
は、複数のスキーマのいずれかにマッチするデータを検証する機能です。TypeScriptのunion型と同様の概念ですが、実行時検証が加わることで格段に安全性が向上します。
基本構文と型定義
まずはZodでのスキーマ定義方法を確認しましょう。
typescriptimport { z } from 'zod'
// 物理商品のスキーマ
const physicalProductSchema = z.object({
type: z.literal('physical'),
name: z.string(),
price: z.number().positive(),
weight: z.number().positive(),
shippingCost: z.number().nonnegative()
})
// デジタル商品のスキーマ
const digitalProductSchema = z.object({
type: z.literal('digital'),
name: z.string(),
price: z.number().positive(),
downloadUrl: z.string().url(),
fileSize: z.number().positive()
})
次に、これらをunionで組み合わせます。
typescript// union()を使用した型定義
const productSchema = z.union([
physicalProductSchema,
digitalProductSchema
])
// TypeScript型の自動生成
type Product = z.infer<typeof productSchema>
簡単な例での実装
実際にバリデーションを行ってみましょう。
typescript// 安全なデータ検証
function validateAndProcessProduct(data: unknown) {
try {
const product = productSchema.parse(data)
// 型安全にアクセス可能
console.log(`商品名: ${product.name}`)
console.log(`価格: ${product.price}円`)
return { success: true, product }
} catch (error) {
console.error('バリデーションエラー:', error)
return { success: false, error }
}
}
使用例を見てみましょう。
typescript// 正常なデータの場合
const physicalProductData = {
type: 'physical',
name: 'ノートパソコン',
price: 80000,
weight: 2.5,
shippingCost: 1500
}
const result1 = validateAndProcessProduct(physicalProductData)
// { success: true, product: {...} }
// 不正なデータの場合
const invalidData = {
type: 'physical',
name: 'ノートパソコン',
price: -1000, // 負の値は不正
weight: 2.5
// shippingCostが欠落
}
const result2 = validateAndProcessProduct(invalidData)
// { success: false, error: ZodError }
これにより、外部データを安全に検証し、型安全にアクセスできるようになります。
discriminatedUnion()の威力
union()
でも十分強力ですが、さらに効率的で型安全な方法がdiscriminatedUnion()
です。この機能は、「判別子(discriminator)」と呼ばれる共通のプロパティを使って、より確実で高速な型判定を実現します。
discriminatedUnionの概念と利点
discriminatedUnionは、各スキーマが共通の判別用プロパティを持っている場合に最適化されたunion型です。
typescript// discriminatedUnion()の基本構文
const productSchema = z.discriminatedUnion('type', [
physicalProductSchema,
digitalProductSchema
])
第一引数の'type'
が判別子プロパティです。各スキーマがこのプロパティを持ち、異なるリテラル値を持つ必要があります。
判別プロパティの活用
discriminatedUnionを使った場合の型絞り込みは非常にスマートです。
typescriptfunction processProduct(product: Product) {
switch (product.type) {
case 'physical':
// TypeScriptが自動的にPhysicalProductと推論
console.log(`重量: ${product.weight}kg`)
console.log(`送料: ${product.shippingCost}円`)
break
case 'digital':
// TypeScriptが自動的にDigitalProductと推論
console.log(`ファイルサイズ: ${product.fileSize}MB`)
console.log(`ダウンロードURL: ${product.downloadUrl}`)
break
default:
// 型安全性: 未処理のケースをコンパイル時に検出
const _exhaustive: never = product
throw new Error('未処理の商品タイプです')
}
}
パフォーマンス面でも大きな利点があります。
# | 機能 | union() | discriminatedUnion() |
---|---|---|---|
1 | 型判定速度 | 順次チェック | 判別子による高速判定 |
2 | エラーメッセージ | 汎用的 | より具体的 |
3 | TypeScript推論 | 複雑 | 明確で確実 |
4 | パフォーマンス | 中程度 | 高速 |
discriminatedUnionは、最初に判別子をチェックして適切なスキーマを選択するため、全てのスキーマを順次試行するunion()よりも効率的です。
実用的な使用例
イベント処理システムでの活用例を見てみましょう。
typescript// イベントスキーマの定義
const userClickEventSchema = z.object({
eventType: z.literal('click'),
timestamp: z.date(),
elementId: z.string(),
coordinates: z.object({
x: z.number(),
y: z.number()
})
})
const pageViewEventSchema = z.object({
eventType: z.literal('pageView'),
timestamp: z.date(),
url: z.string().url(),
referrer: z.string().optional()
})
const formSubmitEventSchema = z.object({
eventType: z.literal('formSubmit'),
timestamp: z.date(),
formId: z.string(),
formData: z.record(z.unknown())
})
これらをdiscriminatedUnionで統合します。
typescript// イベントスキーマの統合
const eventSchema = z.discriminatedUnion('eventType', [
userClickEventSchema,
pageViewEventSchema,
formSubmitEventSchema
])
type Event = z.infer<typeof eventSchema>
イベント処理関数では、型安全で効率的な分岐処理が可能になります。
typescriptfunction handleEvent(event: Event) {
switch (event.eventType) {
case 'click':
// クリックイベント固有のプロパティに安全にアクセス
trackClick(event.elementId, event.coordinates)
break
case 'pageView':
// ページビューイベント固有のプロパティに安全にアクセス
trackPageView(event.url, event.referrer)
break
case 'formSubmit':
// フォーム送信イベント固有のプロパティに安全にアクセス
processFormSubmission(event.formId, event.formData)
break
}
}
これにより、実行時の型安全性とコンパイル時の型推論の両方を最高レベルで実現できます。
具体例
理論だけでは実感が湧かないかもしれません。ここからは、実際の開発現場でよく遭遇する2つのケースを題材に、union型とdiscriminatedUnion型の実用的な活用方法を詳しく見ていきましょう。
APIレスポンスの型定義
成功・失敗レスポンスの型分岐
Web APIを設計する際、成功時と失敗時で全く異なる構造のレスポンスを返すことが一般的です。従来の方法では型安全性に課題がありましたが、discriminatedUnionを使うことで優雅に解決できます。
まず、APIレスポンスのスキーマを定義しましょう。
typescriptimport { z } from 'zod'
// 成功レスポンスのスキーマ
const successResponseSchema = z.object({
status: z.literal('success'),
data: z.object({
id: z.string(),
username: z.string(),
email: z.string().email(),
createdAt: z.string().datetime()
}),
message: z.string()
})
// エラーレスポンスのスキーマ
const errorResponseSchema = z.object({
status: z.literal('error'),
error: z.object({
code: z.string(),
message: z.string(),
details: z.array(z.string()).optional()
})
})
次に、これらをdiscriminatedUnionで統合します。
typescript// APIレスポンススキーマの統合
const apiResponseSchema = z.discriminatedUnion('status', [
successResponseSchema,
errorResponseSchema
])
type ApiResponse = z.infer<typeof apiResponseSchema>
リアルなAPI設計での活用
実際のAPI呼び出し関数を実装してみましょう。
typescript// ユーザー作成API関数
async function createUser(userData: {
username: string
email: string
password: string
}): Promise<ApiResponse> {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
})
const rawData = await response.json()
// Zodによる実行時バリデーション
return apiResponseSchema.parse(rawData)
} catch (error) {
// ネットワークエラーなどの場合のフォールバック
return {
status: 'error',
error: {
code: 'NETWORK_ERROR',
message: 'ネットワークエラーが発生しました'
}
}
}
}
呼び出し側では、型安全にレスポンスを処理できます。
typescript// API呼び出しと結果処理
async function handleUserCreation(userData: any) {
const result = await createUser(userData)
switch (result.status) {
case 'success':
// 成功時の処理:dataプロパティに安全にアクセス
console.log(`ユーザー作成成功: ${result.data.username}`)
console.log(`ユーザーID: ${result.data.id}`)
// UIの更新
showSuccessMessage(result.message)
redirectToUserProfile(result.data.id)
break
case 'error':
// エラー時の処理:errorプロパティに安全にアクセス
console.error(`エラーコード: ${result.error.code}`)
console.error(`エラーメッセージ: ${result.error.message}`)
// 詳細なエラー情報の表示
if (result.error.details) {
result.error.details.forEach(detail => {
console.error(`詳細: ${detail}`)
})
}
// UIの更新
showErrorMessage(result.error.message)
break
}
}
以下の図で、APIレスポンス処理のフローを可視化します。
mermaidflowchart TD
api_call[API呼び出し] --> raw_response[生レスポンス]
raw_response --> zod_validation{Zod バリデーション}
zod_validation -->|成功| validated_data[検証済みデータ]
zod_validation -->|失敗| validation_error[バリデーションエラー]
validated_data --> status_check{ステータス確認}
status_check -->|success| success_handling[成功時処理]
status_check -->|error| error_handling[エラー時処理]
success_handling --> ui_update_success[成功UI更新]
error_handling --> ui_update_error[エラーUI更新]
validation_error --> fallback[フォールバック処理]
fallback --> ui_update_error
この実装により、以下のメリットを享受できます:
図で理解できる要点
- APIレスポンスの型安全性が実行時まで保証される
- 成功・失敗の分岐処理がコンパイル時にチェックされる
- 未処理のケースやプロパティアクセスエラーを防げる
フォームデータの動的型定義
入力タイプによる型切り替え
現代のWebアプリケーションでは、ユーザーの選択によってフォームの内容が動的に変化することが一般的です。例えば、支払い方法の選択によって必要な入力項目が変わるケースを考えてみましょう。
まず、各支払い方法のスキーマを定義します。
typescript// クレジットカード支払いのスキーマ
const creditCardPaymentSchema = z.object({
paymentMethod: z.literal('credit_card'),
cardNumber: z.string().regex(/^\d{16}$/, 'カード番号は16桁で入力してください'),
expiryMonth: z.number().min(1).max(12),
expiryYear: z.number().min(2024).max(2034),
cvv: z.string().regex(/^\d{3,4}$/, 'CVVは3-4桁で入力してください'),
holderName: z.string().min(1, 'カード名義人を入力してください')
})
// 銀行振込のスキーマ
const bankTransferSchema = z.object({
paymentMethod: z.literal('bank_transfer'),
bankName: z.string().min(1, '銀行名を入力してください'),
accountNumber: z.string().regex(/^\d{7,8}$/, '口座番号は7-8桁で入力してください'),
accountHolderName: z.string().min(1, '口座名義人を入力してください'),
transferDate: z.string().datetime()
})
// 電子マネー決済のスキーマ
const digitalWalletSchema = z.object({
paymentMethod: z.literal('digital_wallet'),
walletType: z.enum(['paypal', 'apple_pay', 'google_pay']),
walletId: z.string().min(1, 'ウォレットIDを入力してください'),
authToken: z.string().optional()
})
条件付きフィールドの実装
これらをdiscriminatedUnionで統合して、動的なフォームバリデーションシステムを構築します。
typescript// 支払い情報スキーマの統合
const paymentInfoSchema = z.discriminatedUnion('paymentMethod', [
creditCardPaymentSchema,
bankTransferSchema,
digitalWalletSchema
])
type PaymentInfo = z.infer<typeof paymentInfoSchema>
React コンポーネントでの実装例を見てみましょう。
typescriptimport React, { useState } from 'react'
import { z } from 'zod'
// フォームコンポーネント
function PaymentForm() {
const [paymentMethod, setPaymentMethod] = useState<string>('')
const [formData, setFormData] = useState<any>({})
const [errors, setErrors] = useState<string[]>([])
// フォーム送信処理
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
try {
// 選択された支払い方法に応じたバリデーション
const validatedData = paymentInfoSchema.parse({
paymentMethod,
...formData
})
console.log('バリデーション成功:', validatedData)
processPayment(validatedData)
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessages = error.errors.map(err => err.message)
setErrors(errorMessages)
}
}
}
return (
<form onSubmit={handleSubmit}>
{/* 支払い方法選択 */}
<div>
<label>支払い方法</label>
<select
value={paymentMethod}
onChange={(e) => setPaymentMethod(e.target.value)}
>
<option value="">選択してください</option>
<option value="credit_card">クレジットカード</option>
<option value="bank_transfer">銀行振込</option>
<option value="digital_wallet">電子マネー</option>
</select>
</div>
{/* 条件付きフォームフィールド */}
{renderConditionalFields(paymentMethod, formData, setFormData)}
{/* エラー表示 */}
{errors.length > 0 && (
<div className="errors">
{errors.map((error, index) => (
<p key={index} className="error">{error}</p>
))}
</div>
)}
<button type="submit">支払い情報を送信</button>
</form>
)
}
条件付きフィールドのレンダリング関数です。
typescript// 支払い方法に応じたフィールド表示
function renderConditionalFields(
paymentMethod: string,
formData: any,
setFormData: Function
) {
const updateField = (field: string, value: any) => {
setFormData((prev: any) => ({ ...prev, [field]: value }))
}
switch (paymentMethod) {
case 'credit_card':
return (
<div>
<input
type="text"
placeholder="カード番号 (16桁)"
value={formData.cardNumber || ''}
onChange={(e) => updateField('cardNumber', e.target.value)}
/>
<input
type="number"
placeholder="有効期限月 (1-12)"
value={formData.expiryMonth || ''}
onChange={(e) => updateField('expiryMonth', parseInt(e.target.value))}
/>
<input
type="number"
placeholder="有効期限年"
value={formData.expiryYear || ''}
onChange={(e) => updateField('expiryYear', parseInt(e.target.value))}
/>
<input
type="text"
placeholder="CVV"
value={formData.cvv || ''}
onChange={(e) => updateField('cvv', e.target.value)}
/>
<input
type="text"
placeholder="カード名義人"
value={formData.holderName || ''}
onChange={(e) => updateField('holderName', e.target.value)}
/>
</div>
)
case 'bank_transfer':
return (
<div>
<input
type="text"
placeholder="銀行名"
value={formData.bankName || ''}
onChange={(e) => updateField('bankName', e.target.value)}
/>
<input
type="text"
placeholder="口座番号 (7-8桁)"
value={formData.accountNumber || ''}
onChange={(e) => updateField('accountNumber', e.target.value)}
/>
<input
type="text"
placeholder="口座名義人"
value={formData.accountHolderName || ''}
onChange={(e) => updateField('accountHolderName', e.target.value)}
/>
<input
type="datetime-local"
value={formData.transferDate || ''}
onChange={(e) => updateField('transferDate', e.target.value)}
/>
</div>
)
case 'digital_wallet':
return (
<div>
<select
value={formData.walletType || ''}
onChange={(e) => updateField('walletType', e.target.value)}
>
<option value="">ウォレット種類を選択</option>
<option value="paypal">PayPal</option>
<option value="apple_pay">Apple Pay</option>
<option value="google_pay">Google Pay</option>
</select>
<input
type="text"
placeholder="ウォレットID"
value={formData.walletId || ''}
onChange={(e) => updateField('walletId', e.target.value)}
/>
</div>
)
default:
return null
}
}
支払い処理関数では、型安全にデータを処理できます。
typescript// 型安全な支払い処理
function processPayment(paymentInfo: PaymentInfo) {
switch (paymentInfo.paymentMethod) {
case 'credit_card':
// クレジットカード固有の処理
console.log(`カード処理: ${paymentInfo.cardNumber}`)
console.log(`名義人: ${paymentInfo.holderName}`)
// 実際のカード決済API呼び出し
break
case 'bank_transfer':
// 銀行振込固有の処理
console.log(`銀行: ${paymentInfo.bankName}`)
console.log(`口座: ${paymentInfo.accountNumber}`)
// 銀行振込の処理
break
case 'digital_wallet':
// 電子マネー固有の処理
console.log(`ウォレット: ${paymentInfo.walletType}`)
console.log(`ID: ${paymentInfo.walletId}`)
// 電子マネー決済API呼び出し
break
}
}
この実装により、以下の利点を得られます。
# | 利点 | 説明 |
---|---|---|
1 | 型安全性 | 選択した支払い方法に応じて必要なフィールドが自動的に型チェックされる |
2 | バリデーション | 各支払い方法特有の検証ルールが適用される |
3 | 保守性 | 新しい支払い方法の追加が容易 |
4 | UX向上 | 不要なフィールドが表示されず、ユーザーの混乱を防げる |
この方法により、複雑で動的なフォームでも、型安全性を保ちながら柔軟な実装が可能になります。
まとめ
本記事では、Zodのunion()
とdiscriminatedUnion()
を使った柔軟な型定義について、基本概念から実践的な活用方法まで幅広く解説してきました。
重要なポイントの振り返り
Zodのunion型機能は、従来のTypeScriptだけでは解決が困難だった課題を見事に解決してくれます。特に以下の3つの側面で大きな価値を提供しています。
1. 型安全性の向上
従来の型アサーションや複雑な型ガードに頼る必要がなくなり、実行時までしっかりと型安全性が保証されます。外部APIからの予期しないデータや、ユーザー入力の検証において、バグを未然に防げるでしょう。
2. 開発効率の向上
discriminatedUnion()
による最適化された型判定により、パフォーマンスと可読性を両立できます。switch文による分岐処理で、TypeScriptが自動的に型を絞り込んでくれるため、IDE の補完機能も最大限活用できるのです。
3. 保守性の確保
新しい型パターンの追加や変更が容易になり、長期的なメンテナンスコストを大幅に削減できます。exhaustive checkingにより、型の追加時に対応漏れがないかもコンパイル時に確認できるのは心強い限りです。
使い分けの指針
union()とdiscriminatedUnion()の選択については、以下の基準で判断するとよいでしょう。
union()を選ぶべきケース
- 型の数が少ない(2-3個程度)
- 判別可能なプロパティがない
- シンプルな型合成で十分
discriminatedUnion()を選ぶべきケース
- 型の数が多い(4個以上)
- 共通の判別プロパティが存在する
- 高いパフォーマンスが求められる
- 型安全な分岐処理を多用する
実践への第一歩
まずは小さなプロジェクトから始めて、APIレスポンス処理やフォームバリデーションといった身近な場面で活用してみることをお勧めします。実際に手を動かして体験することで、union型の威力を実感していただけるはずです。
型安全性と柔軟性を両立するZodのunion機能を駆使して、より堅牢で保守性の高いアプリケーションを開発していきましょう。きっと、従来の開発スタイルでは実現できなかった新たな可能性に気づかれることでしょう。
関連リンク
公式ドキュメント
- Zod 公式サイト
- Zod GitHub リポジトリ
- TypeScript 公式ドキュメント - Union Types
- TypeScript 公式ドキュメント - Discriminated Unions
学習リソース
- Zod Documentation - Union types
- Zod Documentation - Discriminated unions
- TypeScript Handbook - Advanced Types
関連技術・ライブラリ
- React Hook Form + Zod
- tRPC - ZodによるEnd-to-end型安全なAPI開発
- Prisma - Zodと組み合わせたデータベース操作
- Next.js API Routes - ZodによるAPI検証
コミュニティ・参考記事
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来