TypeScript リファクタリング技法:既存コードを安全に型安全化する手順

既存の JavaScript プロジェクトを TypeScript に移行することは、開発チームにとって大きな挑戦です。しかし、適切な戦略と段階的なアプローチを取ることで、リスクを最小限に抑えながら型安全性の恩恵を受けることができます。
本記事では、実際のプロジェクトで適用できる具体的なリファクタリング技法を、豊富なコードサンプルとともに解説します。単なる理論ではなく、明日から使える実践的な手順をお伝えしますので、ぜひ最後までお付き合いください。
リファクタリング戦略の設計
型安全化プロジェクトの成功は、最初の戦略設計で決まります。闇雲に始めるのではなく、しっかりとした計画を立てることが重要ですね。
移行計画の立案とリスク評価
まずは現状分析から始めましょう。プロジェクトの規模や複雑さを正確に把握することが、適切な移行戦略を選択する第一歩です。
typescript// 移行対象の分析スクリプト
interface ProjectAnalysis {
totalFiles: number;
jsFiles: number;
nodeModules: string[];
externalDependencies: string[];
complexityScore: number;
}
class MigrationAnalyzer {
private projectPath: string;
constructor(projectPath: string) {
this.projectPath = projectPath;
}
// プロジェクトの複雑度を分析
async analyzeProject(): Promise<ProjectAnalysis> {
const files = await this.scanFiles();
const dependencies = await this.analyzeDependencies();
return {
totalFiles: files.length,
jsFiles: files.filter((f) => f.endsWith('.js'))
.length,
nodeModules: dependencies.internal,
externalDependencies: dependencies.external,
complexityScore: this.calculateComplexity(
files,
dependencies
),
};
}
// 複雑度計算(独自指標)
private calculateComplexity(
files: string[],
deps: { internal: string[]; external: string[] }
): number {
const fileComplexity = files.length * 0.1;
const depComplexity = deps.external.length * 0.3;
const internalComplexity = deps.internal.length * 0.2;
return Math.round(
fileComplexity + depComplexity + internalComplexity
);
}
private async scanFiles(): Promise<string[]> {
// ファイルスキャンの実装
// 実際のプロジェクトでは fs や glob を使用
return [];
}
private async analyzeDependencies(): Promise<{
internal: string[];
external: string[];
}> {
// 依存関係の分析実装
return { internal: [], external: [] };
}
}
リスク評価では、以下の観点を重視します:
リスク項目 | 評価基準 | 対策 |
---|---|---|
コード複雑度 | 循環参照、深いネスト | 段階的リファクタリング |
外部依存性 | 型定義の有無 | 型定義作成・ラッパー実装 |
チーム習熟度 | TypeScript 経験 | 研修・ペアプログラミング |
テストカバレッジ | 既存テストの品質 | テスト強化・型テスト追加 |
typescript// リスク評価の実装例
interface RiskAssessment {
category: 'low' | 'medium' | 'high' | 'critical';
factors: string[];
mitigation: string[];
estimatedEffort: number; // 人日
}
class RiskEvaluator {
// 複雑度ベースのリスク評価
evaluateComplexity(
analysis: ProjectAnalysis
): RiskAssessment {
if (analysis.complexityScore < 10) {
return {
category: 'low',
factors: [
'小規模プロジェクト',
'依存関係がシンプル',
],
mitigation: [
'一括移行可能',
'TypeScript設定の最適化',
],
estimatedEffort: analysis.jsFiles * 0.5,
};
}
if (analysis.complexityScore < 30) {
return {
category: 'medium',
factors: ['中規模プロジェクト', '適度な外部依存'],
mitigation: [
'段階的移行',
'モジュール単位での型付け',
],
estimatedEffort: analysis.jsFiles * 1.2,
};
}
return {
category: 'high',
factors: ['大規模・複雑な構造', '多数の外部依存'],
mitigation: [
'フェーズ分割',
'専門チーム編成',
'リスク監視',
],
estimatedEffort: analysis.jsFiles * 2.5,
};
}
// 依存関係リスクの評価
evaluateDependencyRisk(dependencies: string[]): {
untypedLibraries: string[];
migrationPriority: 'high' | 'medium' | 'low';
} {
const untypedLibraries = dependencies.filter(
(dep) => !this.hasTypeDefinitions(dep)
);
const riskRatio =
untypedLibraries.length / dependencies.length;
return {
untypedLibraries,
migrationPriority:
riskRatio > 0.3
? 'high'
: riskRatio > 0.1
? 'medium'
: 'low',
};
}
private hasTypeDefinitions(packageName: string): boolean {
// @types パッケージの存在確認
// 実際の実装では npm registry API を使用
return true; // プレースホルダー
}
}
段階的導入のロードマップ策定
成功する移行プロジェクトは、必ず段階的なアプローチを取ります。一気に全てを変更しようとすると、予期しない問題に直面することが多いんです。
typescript// 移行ロードマップの定義
interface MigrationPhase {
name: string;
description: string;
targets: string[];
dependencies: string[];
estimatedDuration: number; // 週数
successCriteria: string[];
}
class MigrationRoadmap {
private phases: MigrationPhase[] = [];
// 基本的な4フェーズ構成
createStandardRoadmap(
analysis: ProjectAnalysis
): MigrationPhase[] {
return [
this.createFoundationPhase(),
this.createCoreModulesPhase(analysis),
this.createIntegrationPhase(analysis),
this.createOptimizationPhase(),
];
}
private createFoundationPhase(): MigrationPhase {
return {
name: 'フェーズ1: 基盤整備',
description: 'TypeScript環境の構築と基本設定',
targets: [
'tsconfig.json設定',
'型定義ファイルの準備',
'ビルドスクリプトの更新',
'エディタ設定の統一',
],
dependencies: [],
estimatedDuration: 1,
successCriteria: [
'TypeScriptコンパイルが通る',
'IDE補完が動作する',
'CI/CDパイプラインが正常動作',
],
};
}
private createCoreModulesPhase(
analysis: ProjectAnalysis
): MigrationPhase {
const coreModules = this.identifyCoreModules(analysis);
return {
name: 'フェーズ2: コアモジュール移行',
description: 'ビジネスロジックの中核部分を型安全化',
targets: coreModules,
dependencies: ['フェーズ1'],
estimatedDuration: Math.ceil(coreModules.length / 3),
successCriteria: [
'コアモジュールの型安全性確保',
'既存テストが全て通過',
'any型使用率30%以下',
],
};
}
private createIntegrationPhase(
analysis: ProjectAnalysis
): MigrationPhase {
return {
name: 'フェーズ3: 統合・拡張',
description: '外部ライブラリ統合と型定義の充実',
targets: [
'外部ライブラリの型対応',
'APIレスポンスの型定義',
'状態管理の型安全化',
],
dependencies: ['フェーズ2'],
estimatedDuration: 3,
successCriteria: [
'外部依存の型対応完了',
'ランタイムエラー50%削減',
'コードカバレッジ80%以上',
],
};
}
private createOptimizationPhase(): MigrationPhase {
return {
name: 'フェーズ4: 最適化・品質向上',
description: '型システムの活用とパフォーマンス最適化',
targets: [
'高度な型機能の活用',
'パフォーマンス最適化',
'メンテナンス性向上',
],
dependencies: ['フェーズ3'],
estimatedDuration: 2,
successCriteria: [
'any型使用率5%以下',
'コンパイル時間最適化',
'チーム生産性向上',
],
};
}
// コアモジュールの特定
private identifyCoreModules(
analysis: ProjectAnalysis
): string[] {
// 依存関係グラフから重要度の高いモジュールを特定
// 実際の実装では静的解析ツールを使用
return ['src/models/', 'src/services/', 'src/utils/'];
}
// 移行進捗の追跡
trackProgress(currentPhase: string): {
completionRate: number;
nextMilestones: string[];
blockers: string[];
} {
const phaseIndex = this.phases.findIndex((p) =>
p.name.includes(currentPhase)
);
const completedPhases = phaseIndex;
const totalPhases = this.phases.length;
return {
completionRate: (completedPhases / totalPhases) * 100,
nextMilestones:
this.phases[phaseIndex]?.successCriteria || [],
blockers: [], // 実際の実装では課題管理システムと連携
};
}
}
実際のプロジェクトでは、チーム規模やビジネス要件に応じてフェーズ設計をカスタマイズする必要があります:
typescript// カスタムロードマップ作成の例
interface TeamContext {
size: number;
tsExperience: 'beginner' | 'intermediate' | 'advanced';
businessPressure: 'low' | 'medium' | 'high';
testCoverage: number;
}
class CustomRoadmapBuilder {
// チーム状況に応じたロードマップ調整
adaptRoadmap(
baseRoadmap: MigrationPhase[],
context: TeamContext
): MigrationPhase[] {
return baseRoadmap.map((phase) => {
const adjustedPhase = { ...phase };
// チーム経験レベルに応じた調整
if (context.tsExperience === 'beginner') {
adjustedPhase.estimatedDuration *= 1.5;
adjustedPhase.targets.unshift('TypeScript研修');
}
// ビジネス圧力に応じた調整
if (context.businessPressure === 'high') {
adjustedPhase.targets = this.prioritizeHighImpact(
adjustedPhase.targets
);
}
// テストカバレッジに応じた調整
if (context.testCoverage < 60) {
adjustedPhase.targets.push('テストカバレッジ向上');
adjustedPhase.estimatedDuration += 1;
}
return adjustedPhase;
});
}
private prioritizeHighImpact(
targets: string[]
): string[] {
// ビジネス価値の高い項目を優先
const priorityOrder = [
'ビジネスロジック',
'API',
'データモデル',
'ユーティリティ',
];
return targets.sort((a, b) => {
const aIndex = priorityOrder.findIndex((p) =>
a.includes(p)
);
const bIndex = priorityOrder.findIndex((p) =>
b.includes(p)
);
return aIndex - bIndex;
});
}
}
このような計画的なアプローチにより、リスクを最小化しながら確実に型安全化を進めることができます。次は、実際の移行作業における基礎工程について詳しく見ていきましょう。
型安全化の基礎工程
戦略が決まったら、いよいよ実装フェーズに入ります。ここでは最も重要な基礎工程である、any 型の排除と型推論の活用について実践的な手法をご紹介します。
any 型の段階的排除手法
any 型は「型安全性の敵」とも言える存在ですが、移行初期段階では避けられない場合があります。重要なのは、計画的に段階を踏んで排除していくことです。
typescript// any型の段階的排除戦略
class AnyTypeEliminator {
// 段階1: any型の可視化と分類
analyzeAnyUsage(sourceCode: string): {
explicit: AnyUsage[]; // 明示的なany
implicit: AnyUsage[]; // 暗黙的なany
necessary: AnyUsage[]; // 一時的に必要なany
} {
// 実際の実装ではTypeScript Compiler APIを使用
return {
explicit: this.findExplicitAny(sourceCode),
implicit: this.findImplicitAny(sourceCode),
necessary: this.findNecessaryAny(sourceCode),
};
}
// 段階2: 優先度に基づく排除計画
createEliminationPlan(analysis: any): EliminationStep[] {
return [
{
phase: 'immediate',
targets: analysis.explicit.filter(
(usage: any) => usage.complexity === 'simple'
),
strategy: 'direct_typing',
},
{
phase: 'short_term',
targets: analysis.implicit,
strategy: 'type_inference_enhancement',
},
{
phase: 'long_term',
targets: analysis.explicit.filter(
(usage: any) => usage.complexity === 'complex'
),
strategy: 'gradual_narrowing',
},
];
}
// 段階3: 実際の排除実装
eliminateAnyType(usage: AnyUsage): RefactoringResult {
switch (usage.context) {
case 'function_parameter':
return this.refactorFunctionParameter(usage);
case 'object_property':
return this.refactorObjectProperty(usage);
case 'array_element':
return this.refactorArrayElement(usage);
default:
return this.refactorGeneric(usage);
}
}
private findExplicitAny(sourceCode: string): AnyUsage[] {
// TypeScript ASTを解析してexplicit anyを検出
return [];
}
private findImplicitAny(sourceCode: string): AnyUsage[] {
// 型推論が失敗してimplicit anyになっている箇所を検出
return [];
}
private findNecessaryAny(sourceCode: string): AnyUsage[] {
// 外部ライブラリなど、一時的にanyが必要な箇所を識別
return [];
}
}
interface AnyUsage {
location: { file: string; line: number; column: number };
context:
| 'function_parameter'
| 'object_property'
| 'array_element'
| 'other';
complexity: 'simple' | 'medium' | 'complex';
reason: string;
}
interface EliminationStep {
phase: 'immediate' | 'short_term' | 'long_term';
targets: AnyUsage[];
strategy: string;
}
interface RefactoringResult {
success: boolean;
newCode: string;
appliedTypes: string[];
remainingIssues: string[];
}
実際の排除作業では、以下のようなパターンで段階的に進めます:
typescript// パターン1: 単純な値の型付け
// Before: any型使用
function processData(data: any): any {
return data.map((item: any) => item.name);
}
// After: 適切な型定義
interface DataItem {
name: string;
id: number;
status: 'active' | 'inactive';
}
function processData(data: DataItem[]): string[] {
return data.map((item) => item.name);
}
typescript// パターン2: 複雑なオブジェクトの段階的型付け
// Before: any型のオブジェクト
const userConfig: any = {
theme: 'dark',
language: 'ja',
features: {
notifications: true,
analytics: false,
},
};
// Step 1: 部分的な型定義
interface PartialUserConfig {
theme?: string;
language?: string;
features?: any; // まだ詳細は未定義
}
// Step 2: より詳細な型定義
interface UserFeatures {
notifications: boolean;
analytics: boolean;
experiments?: string[];
}
interface UserConfig {
theme: 'light' | 'dark' | 'auto';
language: 'en' | 'ja' | 'zh';
features: UserFeatures;
}
// Step 3: 完全な型安全性
const userConfig: UserConfig = {
theme: 'dark',
language: 'ja',
features: {
notifications: true,
analytics: false,
},
};
typescript// パターン3: 外部APIレスポンスの型安全化
// Before: any型でAPIレスポンスを処理
async function fetchUserData(id: string): Promise<any> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// After: 段階的な型定義
// Step 1: 基本構造の定義
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
// Step 2: ユーザーデータの型定義
interface UserData {
id: string;
name: string;
email: string;
profile?: UserProfile;
}
interface UserProfile {
avatar?: string;
bio?: string;
location?: string;
}
// Step 3: 型安全なAPI関数
async function fetchUserData(
id: string
): Promise<ApiResponse<UserData>> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// ランタイム検証も併用
if (!isValidUserData(data.data)) {
throw new Error('Invalid user data format');
}
return data;
}
// 型ガードによる実行時検証
function isValidUserData(data: any): data is UserData {
return (
typeof data === 'object' &&
typeof data.id === 'string' &&
typeof data.name === 'string' &&
typeof data.email === 'string'
);
}
型推論を活用した漸進的型付け
TypeScript の型推論機能を最大限活用することで、明示的な型注針を最小限に抑えながら型安全性を向上させることができます。
typescript// 型推論を活用した段階的改善
class TypeInferenceOptimizer {
// 段階1: 基本的な型推論の活用
optimizeBasicInference(): void {
// Before: 冗長な型注針
const userName: string = 'John Doe';
const userAge: number = 30;
const isActive: boolean = true;
// After: 型推論を活用
const userName = 'John Doe'; // string型と推論
const userAge = 30; // number型と推論
const isActive = true; // boolean型と推論
}
// 段階2: 関数の戻り値型推論
optimizeFunctionInference(): void {
// Before: 明示的な戻り値型
function calculateTotal(prices: number[]): number {
return prices.reduce((sum, price) => sum + price, 0);
}
// After: 戻り値型を推論に委ねる
function calculateTotal(prices: number[]) {
return prices.reduce((sum, price) => sum + price, 0); // number型と推論
}
// より複雑な例: 条件分岐のある関数
function processUser(user: User | null) {
if (!user) {
return { success: false, error: 'User not found' }; // 型が推論される
}
return {
success: true,
data: {
id: user.id,
displayName: user.name.toUpperCase(),
},
}; // 型が推論される
}
}
// 段階3: ジェネリクスと型推論の組み合わせ
optimizeGenericInference(): void {
// 型推論を活用したジェネリック関数
function createResponse<T>(data: T) {
return {
timestamp: Date.now(),
data,
meta: {
type: typeof data,
isArray: Array.isArray(data),
},
};
}
// 使用時に型が自動推論される
const userResponse = createResponse({
id: 1,
name: 'John',
});
// 型: { timestamp: number; data: { id: number; name: string }; meta: ... }
const listResponse = createResponse([
'apple',
'banana',
'cherry',
]);
// 型: { timestamp: number; data: string[]; meta: ... }
}
}
実際のプロジェクトでは、以下のような戦略で型推論を活用します:
typescript// 実践的な型推論活用パターン
class PracticalInferencePatterns {
// パターン1: オブジェクト構築での推論活用
buildUserObject() {
// Before: 複雑な型定義
interface ComplexUserType {
personal: {
name: string;
age: number;
};
professional: {
company: string;
role: string;
};
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
};
}
const user: ComplexUserType = {
personal: { name: 'John', age: 30 },
professional: {
company: 'TechCorp',
role: 'Developer',
},
preferences: { theme: 'dark', notifications: true },
};
// After: ファクトリ関数で型推論を活用
function createUser(
personal: { name: string; age: number },
professional: { company: string; role: string },
preferences: {
theme: 'light' | 'dark';
notifications: boolean;
}
) {
return {
personal,
professional,
preferences,
// 計算プロパティも型推論される
displayName: `${personal.name} (${professional.role})`,
isAdult: personal.age >= 18,
};
}
const user = createUser(
{ name: 'John', age: 30 },
{ company: 'TechCorp', role: 'Developer' },
{ theme: 'dark', notifications: true }
);
// 型が自動的に推論され、型安全性も確保される
}
// パターン2: 配列操作での型推論
processArrayData() {
const rawData = [
{
id: 1,
name: 'Apple',
category: 'fruit',
price: 100,
},
{
id: 2,
name: 'Carrot',
category: 'vegetable',
price: 80,
},
{
id: 3,
name: 'Banana',
category: 'fruit',
price: 120,
},
];
// map, filter, reduceでの型推論活用
const fruits = rawData
.filter((item) => item.category === 'fruit') // 型が推論される
.map((item) => ({
...item,
discountPrice: item.price * 0.9, // number型と推論
}))
.sort((a, b) => a.price - b.price); // 型安全な比較
const totalValue = rawData.reduce((sum, item) => {
return sum + item.price; // number型の計算
}, 0);
}
// パターン3: 非同期処理での型推論
async processAsyncData() {
// Promise型の推論を活用
const fetchUserData = async (id: string) => {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return {
user: data,
timestamp: Date.now(),
cached: false,
};
};
// 戻り値型が自動推論される: Promise<{ user: any; timestamp: number; cached: boolean }>
const userData = await fetchUserData('123');
// さらに型安全性を向上させる場合
const fetchTypedUserData = async (
id: string
): Promise<{
user: UserData;
timestamp: number;
cached: boolean;
}> => {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// 型ガードで安全性を確保
if (!isValidUserData(data)) {
throw new Error('Invalid user data');
}
return {
user: data,
timestamp: Date.now(),
cached: false,
};
};
}
}
型推論の活用により、コードの簡潔性と型安全性を両立できます。次は、さらに高度なリファクタリング技法について見ていきましょう。
コード品質向上のリファクタリング技法
基礎工程が完了したら、より高度な技法を使ってコード品質を向上させていきます。特に型ガードと Union 型の活用は、実行時安全性を大幅に向上させる重要な技法です。
型ガードによる実行時安全性の確保
型ガードは、コンパイル時の型チェックだけでなく、実行時の安全性も確保する強力な仕組みです。リファクタリング時には必須の技法と言えるでしょう。
typescript// 基本的な型ガード実装パターン
class TypeGuardImplementation {
// パターン1: プリミティブ型の型ガード
static isString(value: unknown): value is string {
return typeof value === 'string';
}
static isNumber(value: unknown): value is number {
return typeof value === 'number' && !isNaN(value);
}
static isBoolean(value: unknown): value is boolean {
return typeof value === 'boolean';
}
// パターン2: オブジェクト型の型ガード
static isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'email' in value &&
typeof (value as any).id === 'string' &&
typeof (value as any).name === 'string' &&
typeof (value as any).email === 'string'
);
}
// パターン3: 配列型の型ガード
static isStringArray(value: unknown): value is string[] {
return (
Array.isArray(value) &&
value.every((item) => typeof item === 'string')
);
}
// パターン4: Union型の型ガード
static isSuccessResponse(
response: ApiResponse
): response is SuccessResponse {
return response.success === true && 'data' in response;
}
static isErrorResponse(
response: ApiResponse
): response is ErrorResponse {
return (
response.success === false && 'error' in response
);
}
}
interface User {
id: string;
name: string;
email: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
interface SuccessResponse {
success: true;
data: any;
}
interface ErrorResponse {
success: false;
error: string;
}
実際のリファクタリングでは、以下のような段階的アプローチで型ガードを導入します:
typescript// 段階的な型ガード導入の実例
class ProgressiveTypeGuardRefactoring {
// Before: 型安全性が不十分な外部API処理
async fetchUserDataUnsafe(id: string): Promise<any> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// 型チェックなしで処理
return {
user: data,
displayName: data.name.toUpperCase(),
isActive: data.status === 'active',
};
}
// Step 1: 基本的な型ガード導入
async fetchUserDataBasic(
id: string
): Promise<ProcessedUserData | null> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// 基本的な型チェック
if (!this.isValidUserData(data)) {
console.error('Invalid user data received');
return null;
}
return {
user: data,
displayName: data.name.toUpperCase(),
isActive: data.status === 'active',
};
}
// Step 2: より詳細な型ガード
async fetchUserDataAdvanced(
id: string
): Promise<ProcessedUserData> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(
`HTTP ${response.status}: ${response.statusText}`
);
}
const data = await response.json();
// 詳細な型検証
if (!this.isCompleteUserData(data)) {
throw new Error('Incomplete user data received');
}
return {
user: data,
displayName: data.name.toUpperCase(),
isActive: data.status === 'active',
// 追加の安全な処理
profileUrl:
data.profile?.avatar || '/default-avatar.png',
lastLoginFormatted: this.formatDate(data.lastLogin),
};
}
// 型ガード関数
private isValidUserData(data: any): data is UserData {
return (
typeof data === 'object' &&
data !== null &&
typeof data.id === 'string' &&
typeof data.name === 'string' &&
typeof data.email === 'string' &&
['active', 'inactive', 'suspended'].includes(
data.status
)
);
}
private isCompleteUserData(
data: any
): data is CompleteUserData {
return (
this.isValidUserData(data) &&
(data.profile === null ||
this.isValidProfile(data.profile)) &&
(data.lastLogin === null ||
typeof data.lastLogin === 'string')
);
}
private isValidProfile(
profile: any
): profile is UserProfile {
return (
typeof profile === 'object' &&
profile !== null &&
(profile.avatar === undefined ||
typeof profile.avatar === 'string') &&
(profile.bio === undefined ||
typeof profile.bio === 'string')
);
}
private formatDate(dateString: string | null): string {
if (!dateString) return 'Never';
try {
return new Date(dateString).toLocaleDateString();
} catch {
return 'Invalid date';
}
}
}
interface UserData {
id: string;
name: string;
email: string;
status: 'active' | 'inactive' | 'suspended';
}
interface UserProfile {
avatar?: string;
bio?: string;
}
interface CompleteUserData extends UserData {
profile?: UserProfile | null;
lastLogin?: string | null;
}
interface ProcessedUserData {
user: CompleteUserData;
displayName: string;
isActive: boolean;
profileUrl?: string;
lastLoginFormatted?: string;
}
高度な型ガードパターンでは、より複雑な検証ロジックも型安全に実装できます:
typescript// 高度な型ガードパターン
class AdvancedTypeGuards {
// パターン1: ネストしたオブジェクトの型ガード
static isNestedConfig(value: unknown): value is NestedConfig {
if (typeof value !== 'object' || value === null) return false;
const obj = value as any;
return (
typeof obj.database === 'object' &&
obj.database !== null &&
typeof obj.database.host === 'string' &&
typeof obj.database.port === 'number' &&
typeof obj.database.name === 'string' &&
typeof obj.redis === 'object' &&
obj.redis !== null &&
typeof obj.redis.url === 'string' &&
typeof obj.redis.ttl === 'number' &&
Array.isArray(obj.features) &&
obj.features.every((feature: any) =>
typeof feature === 'object' &&
feature !== null &&
typeof feature.name === 'string' &&
typeof feature.enabled === 'boolean'
)
);
}
// パターン2: 条件付き型ガード
static isAdminUser(user: User): user is AdminUser {
return 'permissions' in user && Array.isArray((user as any).permissions);
}
// パターン3: 複合条件の型ガード
static isValidOrder(value: unknown): value is ValidOrder {
if (!this.isBaseOrder(value)) return false;
const order = value as BaseOrder;
// ビジネスルールの検証
const hasValidItems = order.items.length > 0;
const hasValidTotal = order.total > 0;
const hasValidCustomer = order.customerId.length > 0;
const isNotExpired = new Date(order.createdAt) > new Date(Date.now() - 86400000); // 24時間以内
return hasValidItems && hasValidTotal && hasValidCustomer && isNotExpired;
}
private static isBaseOrder(value: unknown): value is BaseOrder {
if (typeof value !== 'object' || value === null) return false;
const obj = value as any;
return (
typeof obj.id === 'string' &&
typeof obj.customerId === 'string' &&
typeof obj.total === 'number' &&
typeof obj.createdAt === 'string' &&
Array.isArray(obj.items) &&
obj.items.every((item: any) =>
typeof item.id === 'string' &&
typeof item.quantity === 'number' &&
typeof item.price === 'number'
)
);
}
// パターン4: ジェネリック型ガード
static isArrayOf<T>(
value: unknown,
itemGuard: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(itemGuard);
}
// パターン5: 非同期型ガード
static async isValidRemoteUser(data: unknown): Promise<data is RemoteUser> {
if (!this.isBasicUser(data)) return false;
const user = data as BasicUser;
try {
// 外部APIで検証
const response = await fetch(`/api/verify-user/${user.id}`);
const verification = await response.json();
return verification.isValid === true;
} catch {
return false;
}
}
private static isBasicUser(value: unknown): value is BasicUser {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as any).id === 'string' &&
typeof (value as any).email === 'string'
);
}
}
interface NestedConfig {
database: {
host: string;
port: number;
name: string;
};
redis: {
url: string;
ttl: number;
};
features: Array<{
name: string;
enabled: boolean;
}>;
}
interface AdminUser extends User {
permissions: string[];
}
interface BaseOrder {
id: string;
customerId: string;
total: number;
createdAt: string;
items: Array<{
id: string;
quantity: number;
price: number;
}>;
}
interface ValidOrder extends BaseOrder {
// マーカーインターフェース(実際の追加プロパティは任意)
}
interface BasicUser {
id: string;
email: string;
}
interface RemoteUser extends BasicUser {
verified: boolean;
}
Union 型と Narrowing の実装パターン
Union 型と Narrowing を組み合わせることで、柔軟でありながら型安全なコードが実現できます。これはリファクタリング時に特に重要な技法です。
typescript// Union型とNarrowingの実践的な実装
class UnionTypeNarrowingPatterns {
// パターン1: Discriminated Union(判別可能なUnion型)
processApiResponse(
response: ApiResponse
): ProcessedResult {
// Type Narrowingにより型安全な分岐処理
if (response.success) {
// ここではSuccessResponseとして扱われる
return {
status: 'success',
data: response.data,
message: 'Operation completed successfully',
};
} else {
// ここではErrorResponseとして扱われる
return {
status: 'error',
data: null,
message: response.error,
};
}
}
// パターン2: 複雑なUnion型の処理
processUserAction(action: UserAction): ActionResult {
switch (action.type) {
case 'login':
// LoginActionとして扱われる
return this.processLogin(action.credentials);
case 'logout':
// LogoutActionとして扱われる
return this.processLogout(action.sessionId);
case 'update_profile':
// UpdateProfileActionとして扱われる
return this.processProfileUpdate(
action.userId,
action.profileData
);
case 'delete_account':
// DeleteAccountActionとして扱われる
return this.processAccountDeletion(
action.userId,
action.confirmation
);
default:
// TypeScriptが網羅性をチェック
const exhaustiveCheck: never = action;
throw new Error(
`Unhandled action type: ${exhaustiveCheck}`
);
}
}
// パターン3: 条件分岐での型絞り込み
processPayment(payment: PaymentMethod): PaymentResult {
if (payment.type === 'credit_card') {
// CreditCardPaymentとして扱われる
return this.processCreditCard(
payment.cardNumber,
payment.expiryDate,
payment.cvv
);
}
if (payment.type === 'bank_transfer') {
// BankTransferPaymentとして扱われる
return this.processBankTransfer(
payment.accountNumber,
payment.routingNumber
);
}
if (payment.type === 'digital_wallet') {
// DigitalWalletPaymentとして扱われる
return this.processDigitalWallet(
payment.walletId,
payment.provider
);
}
// 網羅性チェック
const exhaustiveCheck: never = payment;
throw new Error(
`Unsupported payment type: ${exhaustiveCheck}`
);
}
// パターン4: 型ガードとNarrowingの組み合わせ
processDataSource(source: DataSource): ProcessedData {
if (this.isFileSource(source)) {
// FileSourceとして扱われる
return this.processFile(
source.filePath,
source.encoding
);
}
if (this.isDatabaseSource(source)) {
// DatabaseSourceとして扱われる
return this.processDatabase(
source.connectionString,
source.query
);
}
if (this.isApiSource(source)) {
// ApiSourceとして扱われる
return this.processApi(source.url, source.headers);
}
throw new Error('Unknown data source type');
}
// 型ガード関数
private isFileSource(
source: DataSource
): source is FileSource {
return source.type === 'file';
}
private isDatabaseSource(
source: DataSource
): source is DatabaseSource {
return source.type === 'database';
}
private isApiSource(
source: DataSource
): source is ApiSource {
return source.type === 'api';
}
// ヘルパーメソッド(実装はプレースホルダー)
private processLogin(credentials: any): ActionResult {
return { success: true, message: 'Login successful' };
}
private processLogout(sessionId: string): ActionResult {
return { success: true, message: 'Logout successful' };
}
private processProfileUpdate(
userId: string,
profileData: any
): ActionResult {
return { success: true, message: 'Profile updated' };
}
private processAccountDeletion(
userId: string,
confirmation: string
): ActionResult {
return { success: true, message: 'Account deleted' };
}
private processCreditCard(
cardNumber: string,
expiryDate: string,
cvv: string
): PaymentResult {
return {
success: true,
transactionId: 'cc_' + Date.now(),
};
}
private processBankTransfer(
accountNumber: string,
routingNumber: string
): PaymentResult {
return {
success: true,
transactionId: 'bt_' + Date.now(),
};
}
private processDigitalWallet(
walletId: string,
provider: string
): PaymentResult {
return {
success: true,
transactionId: 'dw_' + Date.now(),
};
}
private processFile(
filePath: string,
encoding: string
): ProcessedData {
return { type: 'file', rows: 100 };
}
private processDatabase(
connectionString: string,
query: string
): ProcessedData {
return { type: 'database', rows: 1000 };
}
private processApi(
url: string,
headers: any
): ProcessedData {
return { type: 'api', rows: 50 };
}
}
// Union型の定義
type UserAction =
| LoginAction
| LogoutAction
| UpdateProfileAction
| DeleteAccountAction;
interface LoginAction {
type: 'login';
credentials: {
username: string;
password: string;
};
}
interface LogoutAction {
type: 'logout';
sessionId: string;
}
interface UpdateProfileAction {
type: 'update_profile';
userId: string;
profileData: {
name?: string;
email?: string;
bio?: string;
};
}
interface DeleteAccountAction {
type: 'delete_account';
userId: string;
confirmation: string;
}
type PaymentMethod =
| CreditCardPayment
| BankTransferPayment
| DigitalWalletPayment;
interface CreditCardPayment {
type: 'credit_card';
cardNumber: string;
expiryDate: string;
cvv: string;
}
interface BankTransferPayment {
type: 'bank_transfer';
accountNumber: string;
routingNumber: string;
}
interface DigitalWalletPayment {
type: 'digital_wallet';
walletId: string;
provider: 'paypal' | 'apple_pay' | 'google_pay';
}
type DataSource = FileSource | DatabaseSource | ApiSource;
interface FileSource {
type: 'file';
filePath: string;
encoding: 'utf-8' | 'utf-16' | 'ascii';
}
interface DatabaseSource {
type: 'database';
connectionString: string;
query: string;
}
interface ApiSource {
type: 'api';
url: string;
headers: Record<string, string>;
}
interface ProcessedResult {
status: 'success' | 'error';
data: any;
message: string;
}
interface ActionResult {
success: boolean;
message: string;
}
interface PaymentResult {
success: boolean;
transactionId?: string;
}
interface ProcessedData {
type: string;
rows: number;
}
実際のリファクタリングでは、既存の if-else 文や switch 文を段階的に Union 型に置き換えていきます。これにより、コードの可読性と保守性が大幅に向上するんです。
複雑なコードベースの型安全化
基本的なリファクタリング技法をマスターしたら、次は複雑なコードベースに対応する必要があります。特に外部ライブラリとレガシー API の扱いは、実際のプロジェクトで必ず直面する課題です。
外部ライブラリの型定義整備
外部ライブラリに型定義がない場合や、型定義が不完全な場合の対処法を段階的にご紹介します。
typescript// 外部ライブラリの型定義作成パターン
namespace ExternalLibraryTyping {
// パターン1: 基本的な型定義作成
// 例:型定義のないUtilライブラリ
// Step 1: 最小限の型定義から開始
declare module 'legacy-utils' {
export function formatDate(date: any): string;
export function debounce(func: any, delay: number): any;
export function deepClone(obj: any): any;
}
// Step 2: より詳細な型定義に発展
declare module 'legacy-utils' {
export function formatDate(
date: Date | string | number
): string;
export function debounce<
T extends (...args: any[]) => any
>(
func: T,
delay: number
): (...args: Parameters<T>) => void;
export function deepClone<T>(obj: T): T;
// 追加の関数も定義
export function throttle<
T extends (...args: any[]) => any
>(
func: T,
limit: number
): (...args: Parameters<T>) => ReturnType<T>;
export interface ConfigOptions {
dateFormat?: string;
locale?: string;
timezone?: string;
}
export function configure(options: ConfigOptions): void;
}
// Step 3: 完全な型定義とドキュメント
declare module 'legacy-utils' {
/**
* 日付を指定されたフォーマットで文字列に変換
* @param date 変換する日付
* @param format フォーマット文字列(オプション)
*/
export function formatDate(
date: Date | string | number,
format?: string
): string;
/**
* 関数の実行を遅延させるデバウンス機能
* @param func 遅延実行する関数
* @param delay 遅延時間(ミリ秒)
* @param immediate 最初の呼び出しを即座に実行するかどうか
*/
export function debounce<
T extends (...args: any[]) => any
>(
func: T,
delay: number,
immediate?: boolean
): DebouncedFunction<T>;
interface DebouncedFunction<
T extends (...args: any[]) => any
> {
(...args: Parameters<T>): void;
cancel(): void;
flush(): ReturnType<T> | undefined;
}
/**
* オブジェクトの深いコピーを作成
* @param obj コピーするオブジェクト
* @param options コピーオプション
*/
export function deepClone<T>(
obj: T,
options?: CloneOptions
): T;
interface CloneOptions {
includeNonEnumerable?: boolean;
preservePrototype?: boolean;
maxDepth?: number;
}
}
}
実際のプロジェクトでは、型定義の品質を段階的に向上させていくアプローチが有効です:
typescript// 型定義品質向上の実践的アプローチ
class ExternalLibraryManager {
// パターン1: 既存ライブラリのラッパー作成
static createTypedWrapper() {
// Before: 型のないライブラリの直接使用
// import * as utils from 'untyped-library';
// After: 型安全なラッパーの作成
class TypedUtils {
static validateEmail(email: string): boolean {
// 実際の実装では 'untyped-library' を使用
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
static sanitizeHtml(
html: string,
options?: SanitizeOptions
): string {
// 型安全なオプション処理
const defaultOptions: Required<SanitizeOptions> = {
allowedTags: ['p', 'br', 'strong', 'em'],
allowedAttributes: {},
removeEmpty: true,
};
const mergedOptions = {
...defaultOptions,
...options,
};
// ライブラリ呼び出し(型変換を含む)
return this.callUntypedSanitizer(
html,
mergedOptions
);
}
private static callUntypedSanitizer(
html: string,
options: Required<SanitizeOptions>
): string {
// 実際のライブラリ呼び出し
// return untypedLibrary.sanitize(html, options);
return html; // プレースホルダー
}
}
return TypedUtils;
}
// パターン2: 段階的な型定義作成
static createProgressiveTypes() {
// Phase 1: 基本型定義
interface BasicChartConfig {
type: string;
data: any;
options?: any;
}
// Phase 2: より具体的な型定義
interface ImprovedChartConfig {
type: 'line' | 'bar' | 'pie' | 'doughnut';
data: ChartData;
options?: ChartOptions;
}
interface ChartData {
labels: string[];
datasets: Dataset[];
}
interface Dataset {
label: string;
data: number[];
backgroundColor?: string | string[];
borderColor?: string | string[];
}
interface ChartOptions {
responsive?: boolean;
plugins?: {
legend?: {
display?: boolean;
position?: 'top' | 'bottom' | 'left' | 'right';
};
tooltip?: {
enabled?: boolean;
mode?: 'index' | 'dataset' | 'point' | 'nearest';
};
};
scales?: {
x?: ScaleOptions;
y?: ScaleOptions;
};
}
interface ScaleOptions {
display?: boolean;
title?: {
display?: boolean;
text?: string;
};
min?: number;
max?: number;
}
// Phase 3: 完全な型安全ラッパー
class TypedChart {
private chartInstance: any;
constructor(
canvasElement: HTMLCanvasElement,
config: ImprovedChartConfig
) {
// 型検証
this.validateConfig(config);
// 外部ライブラリの呼び出し
this.chartInstance = this.createChartInstance(
canvasElement,
config
);
}
updateData(newData: ChartData): void {
if (!this.isValidChartData(newData)) {
throw new Error('Invalid chart data provided');
}
this.chartInstance.data = newData;
this.chartInstance.update();
}
destroy(): void {
if (this.chartInstance) {
this.chartInstance.destroy();
this.chartInstance = null;
}
}
private validateConfig(
config: ImprovedChartConfig
): void {
if (!config.type || !config.data) {
throw new Error(
'Chart type and data are required'
);
}
if (!this.isValidChartData(config.data)) {
throw new Error('Invalid chart data structure');
}
}
private isValidChartData(
data: any
): data is ChartData {
return (
typeof data === 'object' &&
Array.isArray(data.labels) &&
Array.isArray(data.datasets) &&
data.datasets.every(
(dataset: any) =>
typeof dataset.label === 'string' &&
Array.isArray(dataset.data)
)
);
}
private createChartInstance(
canvas: HTMLCanvasElement,
config: ImprovedChartConfig
): any {
// 実際の実装では外部ライブラリを使用
// return new ExternalChart(canvas, config);
return {
data: config.data,
update: () => {},
destroy: () => {},
};
}
}
return TypedChart;
}
// パターン3: 複数ライブラリの統一インターフェース
static createUnifiedInterface() {
// 複数のHTTPライブラリを統一インターフェースで扱う
abstract class HttpClient {
abstract get<T>(
url: string,
config?: RequestConfig
): Promise<T>;
abstract post<T>(
url: string,
data?: any,
config?: RequestConfig
): Promise<T>;
abstract put<T>(
url: string,
data?: any,
config?: RequestConfig
): Promise<T>;
abstract delete<T>(
url: string,
config?: RequestConfig
): Promise<T>;
}
interface RequestConfig {
headers?: Record<string, string>;
timeout?: number;
retries?: number;
}
// Axiosラッパー
class AxiosHttpClient extends HttpClient {
async get<T>(
url: string,
config?: RequestConfig
): Promise<T> {
// axios実装
return {} as T; // プレースホルダー
}
async post<T>(
url: string,
data?: any,
config?: RequestConfig
): Promise<T> {
// axios実装
return {} as T; // プレースホルダー
}
async put<T>(
url: string,
data?: any,
config?: RequestConfig
): Promise<T> {
// axios実装
return {} as T; // プレースホルダー
}
async delete<T>(
url: string,
config?: RequestConfig
): Promise<T> {
// axios実装
return {} as T; // プレースホルダー
}
}
// Fetchラッパー
class FetchHttpClient extends HttpClient {
async get<T>(
url: string,
config?: RequestConfig
): Promise<T> {
const response = await this.fetchWithConfig(url, {
method: 'GET',
...this.configToFetchOptions(config),
});
return response.json();
}
async post<T>(
url: string,
data?: any,
config?: RequestConfig
): Promise<T> {
const response = await this.fetchWithConfig(url, {
method: 'POST',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
...config?.headers,
},
...this.configToFetchOptions(config),
});
return response.json();
}
async put<T>(
url: string,
data?: any,
config?: RequestConfig
): Promise<T> {
const response = await this.fetchWithConfig(url, {
method: 'PUT',
body: JSON.stringify(data),
headers: {
'Content-Type': 'application/json',
...config?.headers,
},
...this.configToFetchOptions(config),
});
return response.json();
}
async delete<T>(
url: string,
config?: RequestConfig
): Promise<T> {
const response = await this.fetchWithConfig(url, {
method: 'DELETE',
...this.configToFetchOptions(config),
});
return response.json();
}
private async fetchWithConfig(
url: string,
options: RequestInit
): Promise<Response> {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(
`HTTP ${response.status}: ${response.statusText}`
);
}
return response;
}
private configToFetchOptions(
config?: RequestConfig
): RequestInit {
return {
headers: config?.headers,
signal: config?.timeout
? AbortSignal.timeout(config.timeout)
: undefined,
};
}
}
return { HttpClient, AxiosHttpClient, FetchHttpClient };
}
}
interface SanitizeOptions {
allowedTags?: string[];
allowedAttributes?: Record<string, string[]>;
removeEmpty?: boolean;
}
レガシー API の型安全なラッパー実装
レガシー API は多くの場合、一貫性のないレスポンス形式や不完全なドキュメンテーションを持っています。これらを型安全に扱うためのパターンをご紹介します。
typescript// レガシーAPI対応の実装パターン
class LegacyApiWrapper {
// パターン1: レスポンス正規化
static createResponseNormalizer() {
// レガシーAPIのレスポンス形式(統一されていない)
interface LegacyUserResponse1 {
user_id: string;
user_name: string;
email_address: string;
is_active: boolean;
created_date: string;
}
interface LegacyUserResponse2 {
id: number;
username: string;
email: string;
status: 'active' | 'inactive';
createdAt: string;
profile?: {
firstName?: string;
lastName?: string;
};
}
// 正規化された型定義
interface NormalizedUser {
id: string;
username: string;
email: string;
isActive: boolean;
createdAt: Date;
profile: {
firstName: string;
lastName: string;
} | null;
}
class UserResponseNormalizer {
static normalizeV1Response(
response: LegacyUserResponse1
): NormalizedUser {
return {
id: response.user_id,
username: response.user_name,
email: response.email_address,
isActive: response.is_active,
createdAt: new Date(response.created_date),
profile: null,
};
}
static normalizeV2Response(
response: LegacyUserResponse2
): NormalizedUser {
return {
id: response.id.toString(),
username: response.username,
email: response.email,
isActive: response.status === 'active',
createdAt: new Date(response.createdAt),
profile: response.profile
? {
firstName: response.profile.firstName || '',
lastName: response.profile.lastName || '',
}
: null,
};
}
static normalizeResponse(
response: unknown,
version: 'v1' | 'v2'
): NormalizedUser {
if (version === 'v1') {
if (!this.isV1Response(response)) {
throw new Error('Invalid v1 response format');
}
return this.normalizeV1Response(response);
} else {
if (!this.isV2Response(response)) {
throw new Error('Invalid v2 response format');
}
return this.normalizeV2Response(response);
}
}
private static isV1Response(
response: unknown
): response is LegacyUserResponse1 {
return (
typeof response === 'object' &&
response !== null &&
'user_id' in response &&
'user_name' in response &&
'email_address' in response
);
}
private static isV2Response(
response: unknown
): response is LegacyUserResponse2 {
return (
typeof response === 'object' &&
response !== null &&
'id' in response &&
'username' in response &&
'email' in response
);
}
}
return UserResponseNormalizer;
}
// パターン2: エラーハンドリングの統一
static createErrorHandler() {
// レガシーAPIの多様なエラー形式
type LegacyError =
| { error: string; code: number }
| { message: string; status: number }
| { errors: string[]; statusCode: number }
| string;
// 正規化されたエラー型
interface NormalizedError {
message: string;
code: number;
details: string[];
timestamp: Date;
}
class LegacyErrorHandler {
static normalizeError(
error: unknown
): NormalizedError {
const timestamp = new Date();
if (typeof error === 'string') {
return {
message: error,
code: 500,
details: [error],
timestamp,
};
}
if (typeof error === 'object' && error !== null) {
// パターン1: { error: string; code: number }
if ('error' in error && 'code' in error) {
return {
message: (error as any).error,
code: (error as any).code,
details: [(error as any).error],
timestamp,
};
}
// パターン2: { message: string; status: number }
if ('message' in error && 'status' in error) {
return {
message: (error as any).message,
code: (error as any).status,
details: [(error as any).message],
timestamp,
};
}
// パターン3: { errors: string[]; statusCode: number }
if ('errors' in error && 'statusCode' in error) {
const errors = (error as any).errors;
return {
message: Array.isArray(errors)
? errors[0]
: 'Multiple errors',
code: (error as any).statusCode,
details: Array.isArray(errors)
? errors
: [errors],
timestamp,
};
}
}
// フォールバック
return {
message: 'Unknown error occurred',
code: 500,
details: ['Unrecognized error format'],
timestamp,
};
}
static isRetryableError(
error: NormalizedError
): boolean {
const retryableCodes = [429, 500, 502, 503, 504];
return retryableCodes.includes(error.code);
}
static createUserFriendlyMessage(
error: NormalizedError
): string {
switch (error.code) {
case 400:
return 'リクエストに問題があります。入力内容をご確認ください。';
case 401:
return 'ログインが必要です。';
case 403:
return 'この操作を実行する権限がありません。';
case 404:
return '要求されたリソースが見つかりません。';
case 429:
return 'リクエストが多すぎます。しばらく待ってから再試行してください。';
case 500:
return 'サーバーエラーが発生しました。しばらく待ってから再試行してください。';
default:
return error.message;
}
}
}
return LegacyErrorHandler;
}
// パターン3: 完全なAPIクライアント実装
static createComprehensiveClient() {
interface ApiClientConfig {
baseUrl: string;
apiVersion: 'v1' | 'v2';
timeout: number;
maxRetries: number;
authToken?: string;
}
class LegacyApiClient {
private config: ApiClientConfig;
private normalizer: any; // UserResponseNormalizer
private errorHandler: any; // LegacyErrorHandler
constructor(config: ApiClientConfig) {
this.config = config;
this.normalizer =
LegacyApiWrapper.createResponseNormalizer();
this.errorHandler =
LegacyApiWrapper.createErrorHandler();
}
async getUser(
userId: string
): Promise<NormalizedUser> {
const endpoint =
this.config.apiVersion === 'v1'
? `/users/${userId}`
: `/v2/users/${userId}`;
try {
const response = await this.makeRequest(endpoint);
return this.normalizer.normalizeResponse(
response,
this.config.apiVersion
);
} catch (error) {
const normalizedError =
this.errorHandler.normalizeError(error);
if (
this.errorHandler.isRetryableError(
normalizedError
)
) {
return this.retryRequest(() =>
this.getUser(userId)
);
}
throw new Error(
this.errorHandler.createUserFriendlyMessage(
normalizedError
)
);
}
}
async updateUser(
userId: string,
updates: Partial<NormalizedUser>
): Promise<NormalizedUser> {
const endpoint =
this.config.apiVersion === 'v1'
? `/users/${userId}`
: `/v2/users/${userId}`;
// API版本に応じたリクエスト形式変換
const requestBody =
this.config.apiVersion === 'v1'
? this.convertToV1Format(updates)
: this.convertToV2Format(updates);
try {
const response = await this.makeRequest(
endpoint,
{
method: 'PUT',
body: JSON.stringify(requestBody),
headers: {
'Content-Type': 'application/json',
},
}
);
return this.normalizer.normalizeResponse(
response,
this.config.apiVersion
);
} catch (error) {
const normalizedError =
this.errorHandler.normalizeError(error);
throw new Error(
this.errorHandler.createUserFriendlyMessage(
normalizedError
)
);
}
}
private async makeRequest(
endpoint: string,
options: RequestInit = {}
): Promise<any> {
const url = `${this.config.baseUrl}${endpoint}`;
const headers = {
...options.headers,
...(this.config.authToken && {
Authorization: `Bearer ${this.config.authToken}`,
}),
};
const response = await fetch(url, {
...options,
headers,
signal: AbortSignal.timeout(this.config.timeout),
});
if (!response.ok) {
const errorData = await response
.json()
.catch(() => ({}));
throw errorData;
}
return response.json();
}
private async retryRequest<T>(
request: () => Promise<T>,
attempt: number = 1
): Promise<T> {
if (attempt > this.config.maxRetries) {
throw new Error('Max retry attempts exceeded');
}
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
await new Promise((resolve) =>
setTimeout(resolve, delay)
);
try {
return await request();
} catch (error) {
return this.retryRequest(request, attempt + 1);
}
}
private convertToV1Format(
updates: Partial<NormalizedUser>
): any {
return {
...(updates.username && {
user_name: updates.username,
}),
...(updates.email && {
email_address: updates.email,
}),
...(updates.isActive !== undefined && {
is_active: updates.isActive,
}),
};
}
private convertToV2Format(
updates: Partial<NormalizedUser>
): any {
return {
...(updates.username && {
username: updates.username,
}),
...(updates.email && { email: updates.email }),
...(updates.isActive !== undefined && {
status: updates.isActive
? 'active'
: 'inactive',
}),
...(updates.profile && {
profile: updates.profile,
}),
};
}
}
return LegacyApiClient;
}
}
これらの技法により、複雑な外部依存関係も型安全に扱えるようになります。次は、リファクタリング作業を効率化するツールの活用について見ていきましょう。
リファクタリング支援ツールの活用
手動でのリファクタリングには限界があります。効率的な型安全化を実現するためには、適切なツールの活用が不可欠です。
TypeScript Compiler API を使った自動変換
TypeScript Compiler API を活用することで、大規模なコードベースでも一貫性のあるリファクタリングが可能になります。
typescript// TypeScript Compiler APIを使った自動変換ツール
import * as ts from 'typescript';
import * as fs from 'fs';
import * as path from 'path';
class AutoRefactoringTool {
private program: ts.Program;
private checker: ts.TypeChecker;
constructor(configPath: string) {
const configFile = ts.readConfigFile(
configPath,
ts.sys.readFile
);
const parsedConfig = ts.parseJsonConfigFileContent(
configFile.config,
ts.sys,
path.dirname(configPath)
);
this.program = ts.createProgram(
parsedConfig.fileNames,
parsedConfig.options
);
this.checker = this.program.getTypeChecker();
}
// パターン1: any型の自動検出と置換提案
findAndReplaceAnyTypes(): RefactoringResult[] {
const results: RefactoringResult[] = [];
for (const sourceFile of this.program.getSourceFiles()) {
if (sourceFile.isDeclarationFile) continue;
ts.forEachChild(sourceFile, (node) => {
this.visitNode(node, sourceFile, results);
});
}
return results;
}
private visitNode(
node: ts.Node,
sourceFile: ts.SourceFile,
results: RefactoringResult[]
): void {
// any型の検出
if (
ts.isTypeReferenceNode(node) &&
node.typeName.getText() === 'any'
) {
const suggestion = this.generateTypeSuggestion(
node,
sourceFile
);
results.push({
file: sourceFile.fileName,
position: node.getStart(),
length: node.getWidth(),
originalCode: node.getText(),
suggestedCode: suggestion,
confidence: this.calculateConfidence(
node,
sourceFile
),
});
}
// 関数パラメータのany型
if (
ts.isFunctionDeclaration(node) ||
ts.isMethodDeclaration(node)
) {
node.parameters.forEach((param) => {
if (param.type && param.type.getText() === 'any') {
const suggestion = this.inferParameterType(
param,
sourceFile
);
results.push({
file: sourceFile.fileName,
position: param.type!.getStart(),
length: param.type!.getWidth(),
originalCode: 'any',
suggestedCode: suggestion,
confidence:
this.calculateParameterTypeConfidence(param),
});
}
});
}
ts.forEachChild(node, (child) =>
this.visitNode(child, sourceFile, results)
);
}
private generateTypeSuggestion(
node: ts.TypeReferenceNode,
sourceFile: ts.SourceFile
): string {
// コンテキストから型を推論
const parent = node.parent;
if (
ts.isVariableDeclaration(parent) &&
parent.initializer
) {
// 初期化子から型を推論
const type = this.checker.getTypeAtLocation(
parent.initializer
);
return this.typeToString(type);
}
if (ts.isParameter(parent)) {
// 関数の使用例から型を推論
return this.inferParameterTypeFromUsage(parent);
}
return 'unknown'; // フォールバック
}
private inferParameterType(
param: ts.ParameterDeclaration,
sourceFile: ts.SourceFile
): string {
const paramName = param.name.getText();
const functionNode =
param.parent as ts.FunctionDeclaration;
// 関数の呼び出し箇所を検索
const callSites = this.findCallSites(
functionNode,
sourceFile
);
const argumentTypes = this.analyzeArgumentTypes(
callSites,
param
);
return this.unifyTypes(argumentTypes);
}
private findCallSites(
func: ts.FunctionDeclaration,
sourceFile: ts.SourceFile
): ts.CallExpression[] {
const callSites: ts.CallExpression[] = [];
const funcName = func.name?.getText();
if (!funcName) return callSites;
// すべてのソースファイルで関数呼び出しを検索
for (const file of this.program.getSourceFiles()) {
ts.forEachChild(file, function visit(node) {
if (
ts.isCallExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === funcName
) {
callSites.push(node);
}
ts.forEachChild(node, visit);
});
}
return callSites;
}
private analyzeArgumentTypes(
callSites: ts.CallExpression[],
param: ts.ParameterDeclaration
): string[] {
const paramIndex = (
param.parent as ts.FunctionDeclaration
).parameters.indexOf(param);
const types: string[] = [];
callSites.forEach((callSite) => {
if (callSite.arguments[paramIndex]) {
const argType = this.checker.getTypeAtLocation(
callSite.arguments[paramIndex]
);
types.push(this.typeToString(argType));
}
});
return types;
}
private unifyTypes(types: string[]): string {
if (types.length === 0) return 'unknown';
if (types.length === 1) return types[0];
// 共通の型を見つける
const uniqueTypes = [...new Set(types)];
if (uniqueTypes.length === 1) {
return uniqueTypes[0];
}
// Union型として結合
return uniqueTypes.join(' | ');
}
private typeToString(type: ts.Type): string {
return this.checker.typeToString(
type,
undefined,
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope
);
}
private calculateConfidence(
node: ts.TypeReferenceNode,
sourceFile: ts.SourceFile
): number {
// 推論の信頼度を計算(0-1)
let confidence = 0.5; // ベース値
const parent = node.parent;
// 初期化子がある場合は信頼度が高い
if (
ts.isVariableDeclaration(parent) &&
parent.initializer
) {
confidence += 0.3;
}
// リテラル値の場合はさらに信頼度が高い
if (
ts.isVariableDeclaration(parent) &&
parent.initializer &&
ts.isLiteralExpression(parent.initializer)
) {
confidence += 0.2;
}
return Math.min(confidence, 1.0);
}
private calculateParameterTypeConfidence(
param: ts.ParameterDeclaration
): number {
const funcNode = param.parent as ts.FunctionDeclaration;
const callSites = this.findCallSites(
funcNode,
param.getSourceFile()
);
// 呼び出し箇所が多いほど信頼度が高い
const callSiteCount = callSites.length;
const baseConfidence = Math.min(
callSiteCount * 0.1,
0.8
);
return baseConfidence;
}
private inferParameterTypeFromUsage(
param: ts.ParameterDeclaration
): string {
// 実装の詳細は省略
return 'unknown';
}
// パターン2: 型定義の自動生成
generateInterfaceFromUsage(
objectLiteral: ts.ObjectLiteralExpression
): string {
const properties: string[] = [];
objectLiteral.properties.forEach((prop) => {
if (
ts.isPropertyAssignment(prop) &&
ts.isIdentifier(prop.name)
) {
const propName = prop.name.text;
const propType = this.checker.getTypeAtLocation(
prop.initializer
);
const typeString = this.typeToString(propType);
properties.push(` ${propName}: ${typeString};`);
}
});
return `interface GeneratedInterface {\n${properties.join(
'\n'
)}\n}`;
}
// パターン3: 自動適用
applyRefactoring(result: RefactoringResult): void {
const sourceFile = result.file;
const content = fs.readFileSync(sourceFile, 'utf-8');
const newContent =
content.slice(0, result.position) +
result.suggestedCode +
content.slice(result.position + result.length);
fs.writeFileSync(sourceFile, newContent, 'utf-8');
}
}
interface RefactoringResult {
file: string;
position: number;
length: number;
originalCode: string;
suggestedCode: string;
confidence: number;
}
実際のプロジェクトでは、以下のようなスクリプトで自動化できます:
typescript// 自動リファクタリング実行スクリプト
class AutoRefactoringRunner {
static async runRefactoring(
projectPath: string
): Promise<void> {
const tool = new AutoRefactoringTool(
path.join(projectPath, 'tsconfig.json')
);
console.log('🔍 any型の検出を開始...');
const results = tool.findAndReplaceAnyTypes();
console.log(
`📊 ${results.length}件のany型を検出しました`
);
// 信頼度でフィルタリング
const highConfidenceResults = results.filter(
(r) => r.confidence > 0.7
);
const mediumConfidenceResults = results.filter(
(r) => r.confidence > 0.4 && r.confidence <= 0.7
);
console.log(
`✅ 高信頼度: ${highConfidenceResults.length}件`
);
console.log(
`⚠️ 中信頼度: ${mediumConfidenceResults.length}件`
);
// 高信頼度の変更を自動適用
for (const result of highConfidenceResults) {
console.log(
`🔧 自動修正: ${result.file}:${result.position}`
);
console.log(
` ${result.originalCode} → ${result.suggestedCode}`
);
tool.applyRefactoring(result);
}
// 中信頼度の変更はレビュー待ち
if (mediumConfidenceResults.length > 0) {
console.log('\n📝 手動レビューが必要な項目:');
mediumConfidenceResults.forEach((result) => {
console.log(` ${result.file}:${result.position}`);
console.log(
` 提案: ${result.originalCode} → ${result.suggestedCode}`
);
console.log(
` 信頼度: ${(result.confidence * 100).toFixed(
1
)}%\n`
);
});
}
}
}
// 使用例
AutoRefactoringRunner.runRefactoring('./src')
.then(() => console.log('✨ リファクタリング完了'))
.catch((err) => console.error('❌ エラー:', err));
ESLint・Prettier との連携で品質保証
コードフォーマットとリンティングルールを組み合わせることで、一貫性のある高品質な TypeScript コードを維持できます。
json// .eslintrc.json - TypeScript用設定
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"@typescript-eslint/recommended",
"@typescript-eslint/recommended-requiring-type-checking"
],
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
// any型の使用を制限
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/no-unsafe-return": "error",
// 型推論の活用を促進
"@typescript-eslint/no-inferrable-types": "error",
"@typescript-eslint/prefer-as-const": "error",
// 型安全性を向上
"@typescript-eslint/strict-boolean-expressions": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/prefer-optional-chain": "error",
// コード品質の向上
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-duplicate-enum-values": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
// 命名規則の統一
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "interface",
"format": ["PascalCase"],
"prefix": ["I"]
},
{
"selector": "typeAlias",
"format": ["PascalCase"]
},
{
"selector": "enum",
"format": ["PascalCase"]
}
]
},
"overrides": [
{
"files": ["*.test.ts", "*.spec.ts"],
"rules": {
"@typescript-eslint/no-explicit-any": "off"
}
}
]
}
カスタム ESLint ルールも作成できます:
typescript// カスタムESLintルール:外部ライブラリの型チェック
import {
ESLintUtils,
TSESTree,
} from '@typescript-eslint/utils';
const createRule = ESLintUtils.RuleCreator(
(name) => `https://example.com/rule/${name}`
);
export const requireTypedExternalLibs = createRule({
name: 'require-typed-external-libs',
meta: {
type: 'problem',
docs: {
description: '外部ライブラリに型定義を要求する',
recommended: 'error',
},
schema: [
{
type: 'object',
properties: {
allowedUntypedLibs: {
type: 'array',
items: { type: 'string' },
},
},
additionalProperties: false,
},
],
messages: {
missingTypes:
'{{libName}} には型定義が必要です。@types/{{libName}} をインストールするか、型定義ファイルを作成してください。',
},
},
defaultOptions: [{ allowedUntypedLibs: [] }],
create(context, [options]) {
const allowedUntypedLibs =
options.allowedUntypedLibs || [];
return {
ImportDeclaration(node: TSESTree.ImportDeclaration) {
if (
node.source.type === 'Literal' &&
typeof node.source.value === 'string'
) {
const libName = node.source.value;
// node_modulesのライブラリのみチェック
if (
!libName.startsWith('.') &&
!libName.startsWith('/')
) {
if (
!allowedUntypedLibs.includes(libName) &&
!hasTypeDefinition(libName)
) {
context.report({
node: node.source,
messageId: 'missingTypes',
data: { libName },
});
}
}
}
},
};
},
});
function hasTypeDefinition(libName: string): boolean {
try {
// @types パッケージの存在確認
require.resolve(`@types/${libName}`);
return true;
} catch {
try {
// ライブラリ自体に型定義が含まれているかチェック
const packageJson = require(`${libName}/package.json`);
return Boolean(
packageJson.types || packageJson.typings
);
} catch {
return false;
}
}
}
統合的な品質チェックスクリプトも用意できます:
typescript// 品質チェック自動化スクリプト
class QualityAssurance {
static async runFullCheck(
projectPath: string
): Promise<QualityReport> {
const report: QualityReport = {
timestamp: new Date(),
typeCheck: await this.runTypeCheck(projectPath),
linting: await this.runLinting(projectPath),
formatting: await this.runFormatting(projectPath),
coverage: await this.runTypeCoverage(projectPath),
};
return report;
}
private static async runTypeCheck(
projectPath: string
): Promise<CheckResult> {
console.log('🔍 型チェック実行中...');
try {
const { execSync } = await import('child_process');
execSync('yarn tsc --noEmit', {
cwd: projectPath,
stdio: 'pipe',
});
return { success: true, message: '型チェック完了' };
} catch (error: any) {
return {
success: false,
message: '型エラーが検出されました',
details: error.stdout?.toString() || error.message,
};
}
}
private static async runLinting(
projectPath: string
): Promise<CheckResult> {
console.log('📋 ESLintチェック実行中...');
try {
const { execSync } = await import('child_process');
const output = execSync(
'yarn eslint . --ext .ts,.tsx --format json',
{
cwd: projectPath,
stdio: 'pipe',
}
);
const results = JSON.parse(output.toString());
const errorCount = results.reduce(
(sum: number, file: any) => sum + file.errorCount,
0
);
return {
success: errorCount === 0,
message:
errorCount === 0
? 'リンティング完了'
: `${errorCount}個のエラーが見つかりました`,
details:
errorCount > 0
? JSON.stringify(results, null, 2)
: undefined,
};
} catch (error: any) {
return {
success: false,
message: 'ESLintエラー',
details: error.message,
};
}
}
private static async runFormatting(
projectPath: string
): Promise<CheckResult> {
console.log('✨ Prettierチェック実行中...');
try {
const { execSync } = await import('child_process');
execSync('yarn prettier --check .', {
cwd: projectPath,
stdio: 'pipe',
});
return {
success: true,
message: 'フォーマット確認完了',
};
} catch (error: any) {
return {
success: false,
message: 'フォーマットの問題が見つかりました',
details:
'yarn prettier --write . を実行してください',
};
}
}
private static async runTypeCoverage(
projectPath: string
): Promise<CheckResult> {
console.log('📊 型カバレッジ計測中...');
try {
const { execSync } = await import('child_process');
const output = execSync(
'yarn type-coverage --detail',
{
cwd: projectPath,
stdio: 'pipe',
}
);
const result = output.toString();
const match = result.match(/(\d+\.\d+)%/);
const coverage = match ? parseFloat(match[1]) : 0;
return {
success: coverage >= 90, // 90%以上を目標
message: `型カバレッジ: ${coverage}%`,
details: coverage < 90 ? result : undefined,
};
} catch (error: any) {
return {
success: false,
message: '型カバレッジ計測エラー',
details: error.message,
};
}
}
}
interface QualityReport {
timestamp: Date;
typeCheck: CheckResult;
linting: CheckResult;
formatting: CheckResult;
coverage: CheckResult;
}
interface CheckResult {
success: boolean;
message: string;
details?: string;
}
// 使用例
QualityAssurance.runFullCheck('./src').then((report) => {
console.log('\n📋 品質レポート');
console.log('================');
console.log(
`型チェック: ${
report.typeCheck.success ? '✅' : '❌'
} ${report.typeCheck.message}`
);
console.log(
`ESLint: ${report.linting.success ? '✅' : '❌'} ${
report.linting.message
}`
);
console.log(
`Prettier: ${report.formatting.success ? '✅' : '❌'} ${
report.formatting.message
}`
);
console.log(
`型カバレッジ: ${
report.coverage.success ? '✅' : '❌'
} ${report.coverage.message}`
);
const allPassed = [
report.typeCheck,
report.linting,
report.formatting,
report.coverage,
].every((check) => check.success);
console.log(
`\n総合結果: ${allPassed ? '✅ 合格' : '❌ 要改善'}`
);
});
まとめ
本記事では、TypeScript リファクタリングの実践的な手法を段階的にご紹介しました。重要なポイントを振り返ってみましょう。
戦略設計の重要性
成功するリファクタリングプロジェクトは、必ず明確な戦略から始まります。プロジェクトの複雑度を正確に分析し、段階的なロードマップを策定することで、リスクを最小化しながら確実に型安全性を向上させることができます。
基礎工程の着実な実行
any 型の段階的排除と型推論の活用は、すべてのリファクタリング作業の基盤となります。急がば回れの精神で、一つずつ丁寧に型安全化を進めることが、長期的な成功につながります。
高度な技法の習得
型ガードと Union 型・Narrowing は、実行時安全性を確保する強力な武器です。これらの技法をマスターすることで、コンパイル時だけでなく実行時の品質も大幅に向上させることができるでしょう。
複雑な課題への対応
外部ライブラリやレガシー API との統合は避けて通れない課題ですが、適切なラッパー実装とレスポンス正規化により、これらの複雑性も型安全に管理できます。
ツールの効果的活用
TypeScript Compiler API や ESLint・Prettier などのツールを組み合わせることで、手動作業では不可能な規模のリファクタリングも効率的に実行できます。自動化できる部分は積極的に自動化し、人間は創造的な設計判断に集中することが重要です。
TypeScript リファクタリングは一朝一夕で完成するものではありません。しかし、本記事でご紹介した技法を段階的に適用していけば、必ず型安全で保守性の高いコードベースを構築することができます。
最初は小さなモジュールから始めて、徐々に適用範囲を広げていく。そんな着実なアプローチが、最終的に大きな成果を生み出すことでしょう。ぜひ今日から、あなたのプロジェクトでも実践してみてください。
関連リンク
- blog
「アジャイルコーチ」って何する人?チームを最強にする影の立役者の役割と、あなたがコーチになるための道筋
- blog
ペアプロって本当に効果ある?メリットだけじゃない、現場で感じたリアルな課題と乗り越え方
- blog
TDDって結局何がいいの?コードに自信が持てる、テスト駆動開発のはじめの一歩
- blog
「昨日やったこと、今日やること」の報告会じゃない!デイリースクラムをチームのエンジンにするための3つの問いかけ
- blog
燃え尽きるのは誰だ?バーンダウンチャートでプロジェクトの「ヤバさ」をチームで共有する方法
- blog
「誰が、何を、なぜ」が伝わらないユーザーストーリーは無意味。開発者が本当に欲しいストーリーの書き方