Skip to content

AIコード生成チュートリアル

Traeの高度なAIコード生成機能をマスターして、より速く効率的にコードを書きましょう。

概要

TraeのAIコード生成は、単純な自動補完を超えています。以下のことができます:

  • 関数やクラス全体の生成
  • 説明からコンポーネントを作成
  • 完全な機能の構築
  • テストの自動生成
  • ドキュメントの作成
  • 既存コードのリファクタリング

はじめに

コード生成の有効化

  1. 設定を開く: Ctrl+, (Windows/Linux) または Cmd+, (Mac)
  2. 移動: AI > コード生成
  3. 機能を有効化:
    • ✅ 自動補完
    • ✅ 関数生成
    • ✅ コンポーネント足場
    • ✅ テスト生成

コード生成のトリガー

方法1: コメント

typescript
// メールアドレスを検証する関数を生成
// AIがこのコメントの下に関数を生成します

方法2: 関数シグネチャ

typescript
function validateEmail(email: string): boolean {
  // AIが実装を完成させます
}

方法3: AIチャット

  • AIチャットを開く (Ctrl+Shift+A)
  • 特定のコード生成をリクエスト

方法4: コードアクション

  • エディタで右クリック
  • "AIで生成"を選択

基本的なコード生成

1. 関数生成

入力:

typescript
// API呼び出しをデバウンスする関数を作成
function debounce

生成された出力:

typescript
function debounce<T extends (...args: any[]) => any>(
  func: T,
  delay: number
): (...args: Parameters<T>) => void {
  let timeoutId: NodeJS.Timeout;
  
  return (...args: Parameters<T>) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

// 使用例:
const debouncedSearch = debounce((query: string) => {
  console.log('検索中:', query);
}, 300);

2. クラス生成

入力:

typescript
// 認証メソッドを持つUserクラスを作成
class User {

生成された出力:

typescript
class User {
  private id: string;
  private email: string;
  private passwordHash: string;
  private isActive: boolean;
  private createdAt: Date;

  constructor(email: string, password: string) {
    this.id = this.generateId();
    this.email = email;
    this.passwordHash = this.hashPassword(password);
    this.isActive = true;
    this.createdAt = new Date();
  }

  private generateId(): string {
    return Math.random().toString(36).substr(2, 9);
  }

  private hashPassword(password: string): string {
    // 本番環境では、bcryptなどを使用
    return btoa(password + 'salt');
  }

  public authenticate(password: string): boolean {
    return this.passwordHash === this.hashPassword(password);
  }

  public activate(): void {
    this.isActive = true;
  }

  public deactivate(): void {
    this.isActive = false;
  }

  public getProfile(): { id: string; email: string; isActive: boolean } {
    return {
      id: this.id,
      email: this.email,
      isActive: this.isActive
    };
  }
}

Reactコンポーネント生成

1. シンプルなコンポーネント

入力:

typescript
// ローディングスピナーコンポーネントを作成
const LoadingSpinner

生成された出力:

typescript
import React from 'react';
import './LoadingSpinner.css';

interface LoadingSpinnerProps {
  size?: 'small' | 'medium' | 'large';
  color?: string;
  message?: string;
}

const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({ 
  size = 'medium', 
  color = '#3498db',
  message = '読み込み中...'
}) => {
  const sizeClasses = {
    small: 'spinner-small',
    medium: 'spinner-medium',
    large: 'spinner-large'
  };

  return (
    <div className="loading-container">
      <div 
        className={`spinner ${sizeClasses[size]}`}
        style={{ borderTopColor: color }}
      />
      {message && <p className="loading-message">{message}</p>}
    </div>
  );
};

export default LoadingSpinner;

生成されたCSS:

css
/* LoadingSpinner.css */
.loading-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

.spinner {
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

.spinner-small {
  width: 20px;
  height: 20px;
  border-width: 2px;
}

.spinner-medium {
  width: 40px;
  height: 40px;
}

.spinner-large {
  width: 60px;
  height: 60px;
  border-width: 6px;
}

.loading-message {
  margin-top: 10px;
  color: #666;
  font-size: 14px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

2. フックを使った複雑なコンポーネント

入力:

typescript
// デバウンス入力と結果を持つ検索コンポーネントを作成
const SearchComponent

生成された出力:

typescript
import React, { useState, useEffect, useMemo } from 'react';
import { debounce } from '../utils/debounce';

interface SearchResult {
  id: string;
  title: string;
  description: string;
  url?: string;
}

interface SearchComponentProps {
  onSearch: (query: string) => Promise<SearchResult[]>;
  placeholder?: string;
  debounceMs?: number;
  maxResults?: number;
}

const SearchComponent: React.FC<SearchComponentProps> = ({
  onSearch,
  placeholder = '検索...',
  debounceMs = 300,
  maxResults = 10
}) => {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<SearchResult[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const debouncedSearch = useMemo(
    () => debounce(async (searchQuery: string) => {
      if (!searchQuery.trim()) {
        setResults([]);
        return;
      }

      setLoading(true);
      setError(null);

      try {
        const searchResults = await onSearch(searchQuery);
        setResults(searchResults.slice(0, maxResults));
      } catch (err) {
        setError('検索に失敗しました。もう一度お試しください。');
        setResults([]);
      } finally {
        setLoading(false);
      }
    }, debounceMs),
    [onSearch, debounceMs, maxResults]
  );

  useEffect(() => {
    debouncedSearch(query);
  }, [query, debouncedSearch]);

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setQuery(e.target.value);
  };

  const handleResultClick = (result: SearchResult) => {
    if (result.url) {
      window.open(result.url, '_blank');
    }
  };

  return (
    <div className="search-component">
      <div className="search-input-container">
        <input
          type="text"
          value={query}
          onChange={handleInputChange}
          placeholder={placeholder}
          className="search-input"
        />
        {loading && <div className="search-loading">🔍</div>}
      </div>

      {error && (
        <div className="search-error">
          {error}
        </div>
      )}

      {results.length > 0 && (
        <div className="search-results">
          {results.map((result) => (
            <div
              key={result.id}
              className="search-result-item"
              onClick={() => handleResultClick(result)}
            >
              <h4 className="result-title">{result.title}</h4>
              <p className="result-description">{result.description}</p>
            </div>
          ))}
        </div>
      )}

      {query && !loading && results.length === 0 && !error && (
        <div className="no-results">
          "{query}"の結果が見つかりませんでした
        </div>
      )}
    </div>
  );
};

export default SearchComponent;

高度なコード生成

1. API統合

入力:

typescript
// ユーザー管理API用のサービスクラスを作成
class UserService {

生成された出力:

typescript
interface User {
  id: string;
  email: string;
  name: string;
  createdAt: string;
  updatedAt: string;
}

interface CreateUserRequest {
  email: string;
  name: string;
  password: string;
}

interface UpdateUserRequest {
  name?: string;
  email?: string;
}

class UserService {
  private baseUrl: string;
  private apiKey: string;

  constructor(baseUrl: string, apiKey: string) {
    this.baseUrl = baseUrl;
    this.apiKey = apiKey;
  }

  private async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`;
    const config: RequestInit = {
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.apiKey}`,
        ...options.headers,
      },
      ...options,
    };

    const response = await fetch(url, config);

    if (!response.ok) {
      throw new Error(`HTTPエラー! ステータス: ${response.status}`);
    }

    return response.json();
  }

  async getUsers(): Promise<User[]> {
    return this.request<User[]>('/users');
  }

  async getUserById(id: string): Promise<User> {
    return this.request<User>(`/users/${id}`);
  }

  async createUser(userData: CreateUserRequest): Promise<User> {
    return this.request<User>('/users', {
      method: 'POST',
      body: JSON.stringify(userData),
    });
  }

  async updateUser(id: string, userData: UpdateUserRequest): Promise<User> {
    return this.request<User>(`/users/${id}`, {
      method: 'PUT',
      body: JSON.stringify(userData),
    });
  }

  async deleteUser(id: string): Promise<void> {
    await this.request<void>(`/users/${id}`, {
      method: 'DELETE',
    });
  }

  async searchUsers(query: string): Promise<User[]> {
    const params = new URLSearchParams({ q: query });
    return this.request<User[]>(`/users/search?${params}`);
  }
}

export default UserService;

2. カスタムフック生成

入力:

typescript
// フォーム検証用のカスタムフックを作成
function useFormValidation

生成された出力:

typescript
import { useState, useCallback, useMemo } from 'react';

type ValidationRule<T> = {
  required?: boolean;
  minLength?: number;
  maxLength?: number;
  pattern?: RegExp;
  custom?: (value: T) => string | null;
};

type ValidationRules<T> = {
  [K in keyof T]?: ValidationRule<T[K]>;
};

type ValidationErrors<T> = {
  [K in keyof T]?: string;
};

interface UseFormValidationReturn<T> {
  values: T;
  errors: ValidationErrors<T>;
  isValid: boolean;
  isSubmitting: boolean;
  setValue: <K extends keyof T>(field: K, value: T[K]) => void;
  setValues: (values: Partial<T>) => void;
  validateField: <K extends keyof T>(field: K) => boolean;
  validateAll: () => boolean;
  reset: () => void;
  handleSubmit: (onSubmit: (values: T) => Promise<void> | void) => Promise<void>;
}

function useFormValidation<T extends Record<string, any>>(
  initialValues: T,
  validationRules: ValidationRules<T>
): UseFormValidationReturn<T> {
  const [values, setValuesState] = useState<T>(initialValues);
  const [errors, setErrors] = useState<ValidationErrors<T>>({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const validateField = useCallback(<K extends keyof T>(field: K): boolean => {
    const value = values[field];
    const rules = validationRules[field];
    
    if (!rules) return true;

    let error: string | null = null;

    // 必須検証
    if (rules.required && (!value || (typeof value === 'string' && !value.trim()))) {
      error = `${String(field)}は必須です`;
    }
    
    // 文字列検証
    if (!error && typeof value === 'string') {
      if (rules.minLength && value.length < rules.minLength) {
        error = `${String(field)}は${rules.minLength}文字以上である必要があります`;
      }
      
      if (rules.maxLength && value.length > rules.maxLength) {
        error = `${String(field)}は${rules.maxLength}文字以下である必要があります`;
      }
      
      if (rules.pattern && !rules.pattern.test(value)) {
        error = `${String(field)}の形式が無効です`;
      }
    }

    // カスタム検証
    if (!error && rules.custom) {
      error = rules.custom(value);
    }

    setErrors(prev => ({
      ...prev,
      [field]: error || undefined
    }));

    return !error;
  }, [values, validationRules]);

  const validateAll = useCallback((): boolean => {
    const fields = Object.keys(validationRules) as (keyof T)[];
    const results = fields.map(field => validateField(field));
    return results.every(Boolean);
  }, [validateField, validationRules]);

  const setValue = useCallback(<K extends keyof T>(field: K, value: T[K]) => {
    setValuesState(prev => ({ ...prev, [field]: value }));
    
    // ユーザーが入力を開始したらエラーをクリア
    if (errors[field]) {
      setErrors(prev => ({ ...prev, [field]: undefined }));
    }
  }, [errors]);

  const setValues = useCallback((newValues: Partial<T>) => {
    setValuesState(prev => ({ ...prev, ...newValues }));
  }, []);

  const reset = useCallback(() => {
    setValuesState(initialValues);
    setErrors({});
    setIsSubmitting(false);
  }, [initialValues]);

  const handleSubmit = useCallback(async (onSubmit: (values: T) => Promise<void> | void) => {
    if (isSubmitting) return;
    
    setIsSubmitting(true);
    
    try {
      if (validateAll()) {
        await onSubmit(values);
      }
    } catch (error) {
      console.error('フォーム送信エラー:', error);
    } finally {
      setIsSubmitting(false);
    }
  }, [values, validateAll, isSubmitting]);

  const isValid = useMemo(() => {
    return Object.keys(validationRules).every(field => 
      !errors[field as keyof T]
    );
  }, [errors, validationRules]);

  return {
    values,
    errors,
    isValid,
    isSubmitting,
    setValue,
    setValues,
    validateField,
    validateAll,
    reset,
    handleSubmit
  };
}

export default useFormValidation;

// 使用例:
/*
interface LoginForm {
  email: string;
  password: string;
}

const LoginComponent = () => {
  const {
    values,
    errors,
    isValid,
    isSubmitting,
    setValue,
    handleSubmit
  } = useFormValidation<LoginForm>(
    { email: '', password: '' },
    {
      email: {
        required: true,
        pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
        custom: (value) => {
          if (value.includes('test')) {
            return 'テストメールは許可されていません';
          }
          return null;
        }
      },
      password: {
        required: true,
        minLength: 8
      }
    }
  );

  const onSubmit = async (formData: LoginForm) => {
    // ログイン処理
    console.log('ログイン中:', formData);
  };

  return (
    <form onSubmit={(e) => {
      e.preventDefault();
      handleSubmit(onSubmit);
    }}>
      <input
        type="email"
        value={values.email}
        onChange={(e) => setValue('email', e.target.value)}
        placeholder="メール"
      />
      {errors.email && <span>{errors.email}</span>}
      
      <input
        type="password"
        value={values.password}
        onChange={(e) => setValue('password', e.target.value)}
        placeholder="パスワード"
      />
      {errors.password && <span>{errors.password}</span>}
      
      <button type="submit" disabled={!isValid || isSubmitting}>
        {isSubmitting ? 'ログイン中...' : 'ログイン'}
      </button>
    </form>
  );
};
*/

テスト生成

1. ユニットテスト

入力:

typescript
// validateEmail関数のテストを生成
// function validateEmail(email: string): boolean

生成された出力:

typescript
import { validateEmail } from './emailValidator';

describe('validateEmail', () => {
  describe('有効なメール', () => {
    test('標準的なメール形式を受け入れる', () => {
      expect(validateEmail('user@example.com')).toBe(true);
      expect(validateEmail('test.email@domain.org')).toBe(true);
      expect(validateEmail('user+tag@example.co.uk')).toBe(true);
    });

    test('数字を含むメールを受け入れる', () => {
      expect(validateEmail('user123@example.com')).toBe(true);
      expect(validateEmail('123user@example.com')).toBe(true);
    });

    test('特殊文字を含むメールを受け入れる', () => {
      expect(validateEmail('user.name@example.com')).toBe(true);
      expect(validateEmail('user+tag@example.com')).toBe(true);
      expect(validateEmail('user_name@example.com')).toBe(true);
    });

    test('サブドメインを含むメールを受け入れる', () => {
      expect(validateEmail('user@mail.example.com')).toBe(true);
      expect(validateEmail('user@subdomain.domain.com')).toBe(true);
    });
  });

  describe('無効なメール', () => {
    test('@記号がないメールを拒否する', () => {
      expect(validateEmail('userexample.com')).toBe(false);
      expect(validateEmail('user.example.com')).toBe(false);
    });

    test('ドメインがないメールを拒否する', () => {
      expect(validateEmail('user@')).toBe(false);
      expect(validateEmail('user@.')).toBe(false);
    });

    test('ローカル部がないメールを拒否する', () => {
      expect(validateEmail('@example.com')).toBe(false);
      expect(validateEmail('.@example.com')).toBe(false);
    });

    test('無効な文字を含むメールを拒否する', () => {
      expect(validateEmail('user name@example.com')).toBe(false);
      expect(validateEmail('user@exam ple.com')).toBe(false);
      expect(validateEmail('user@example..com')).toBe(false);
    });

    test('空または null の入力を拒否する', () => {
      expect(validateEmail('')).toBe(false);
      expect(validateEmail(' ')).toBe(false);
    });

    test('長すぎるメールを拒否する', () => {
      const longEmail = 'a'.repeat(250) + '@example.com';
      expect(validateEmail(longEmail)).toBe(false);
    });

    test('無効なドメイン形式を拒否する', () => {
      expect(validateEmail('user@example')).toBe(false);
      expect(validateEmail('user@.com')).toBe(false);
      expect(validateEmail('user@example.')).toBe(false);
    });
  });

  describe('エッジケース', () => {
    test('国際化ドメイン名を処理する', () => {
      expect(validateEmail('user@münchen.de')).toBe(true);
    });

    test('引用符で囲まれたローカル部を処理する', () => {
      expect(validateEmail('"user name"@example.com')).toBe(true);
    });

    test('IPアドレスドメインを処理する', () => {
      expect(validateEmail('user@[192.168.1.1]')).toBe(true);
    });
  });
});

2. コンポーネントテスト

入力:

typescript
// SearchComponentのテストを生成

生成された出力:

typescript
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import SearchComponent from './SearchComponent';

const mockSearchResults = [
  {
    id: '1',
    title: '最初の結果',
    description: '最初の結果の説明',
    url: 'https://example.com/1'
  },
  {
    id: '2',
    title: '2番目の結果',
    description: '2番目の結果の説明',
    url: 'https://example.com/2'
  }
];

const mockOnSearch = jest.fn();

describe('SearchComponent', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  test('プレースホルダー付きの検索入力をレンダリングする', () => {
    render(<SearchComponent onSearch={mockOnSearch} placeholder="アイテムを検索..." />);
    
    expect(screen.getByPlaceholderText('アイテムを検索...')).toBeInTheDocument();
  });

  test('デバウンスされた入力でonSearchを呼び出す', async () => {
    mockOnSearch.mockResolvedValue(mockSearchResults);
    
    render(<SearchComponent onSearch={mockOnSearch} debounceMs={300} />);
    
    const input = screen.getByPlaceholderText('検索...');
    
    await userEvent.type(input, 'テストクエリ');
    
    // すぐには呼び出されない
    expect(mockOnSearch).not.toHaveBeenCalled();
    
    // 時間を進める
    jest.advanceTimersByTime(300);
    
    await waitFor(() => {
      expect(mockOnSearch).toHaveBeenCalledWith('テストクエリ');
    });
  });

  test('検索結果を表示する', async () => {
    mockOnSearch.mockResolvedValue(mockSearchResults);
    
    render(<SearchComponent onSearch={mockOnSearch} />);
    
    const input = screen.getByPlaceholderText('検索...');
    await userEvent.type(input, 'テスト');
    
    jest.advanceTimersByTime(300);
    
    await waitFor(() => {
      expect(screen.getByText('最初の結果')).toBeInTheDocument();
      expect(screen.getByText('2番目の結果')).toBeInTheDocument();
      expect(screen.getByText('最初の結果の説明')).toBeInTheDocument();
    });
  });

  test('検索中にローディング状態を表示する', async () => {
    mockOnSearch.mockImplementation(() => new Promise(resolve => 
      setTimeout(() => resolve(mockSearchResults), 1000)
    ));
    
    render(<SearchComponent onSearch={mockOnSearch} />);
    
    const input = screen.getByPlaceholderText('検索...');
    await userEvent.type(input, 'テスト');
    
    jest.advanceTimersByTime(300);
    
    await waitFor(() => {
      expect(screen.getByText('🔍')).toBeInTheDocument();
    });
  });

  test('検索エラーを処理する', async () => {
    mockOnSearch.mockRejectedValue(new Error('検索失敗'));
    
    render(<SearchComponent onSearch={mockOnSearch} />);
    
    const input = screen.getByPlaceholderText('検索...');
    await userEvent.type(input, 'テスト');
    
    jest.advanceTimersByTime(300);
    
    await waitFor(() => {
      expect(screen.getByText('検索に失敗しました。もう一度お試しください。')).toBeInTheDocument();
    });
  });

  test('結果がない場合のメッセージを表示する', async () => {
    mockOnSearch.mockResolvedValue([]);
    
    render(<SearchComponent onSearch={mockOnSearch} />);
    
    const input = screen.getByPlaceholderText('検索...');
    await userEvent.type(input, 'テスト');
    
    jest.advanceTimersByTime(300);
    
    await waitFor(() => {
      expect(screen.getByText('"テスト"の結果が見つかりませんでした')).toBeInTheDocument();
    });
  });

  test('結果の最大数を制限する', async () => {
    const manyResults = Array.from({ length: 20 }, (_, i) => ({
      id: `${i + 1}`,
      title: `結果 ${i + 1}`,
      description: `説明 ${i + 1}`
    }));
    
    mockOnSearch.mockResolvedValue(manyResults);
    
    render(<SearchComponent onSearch={mockOnSearch} maxResults={5} />);
    
    const input = screen.getByPlaceholderText('検索...');
    await userEvent.type(input, 'テスト');
    
    jest.advanceTimersByTime(300);
    
    await waitFor(() => {
      expect(screen.getByText('結果 1')).toBeInTheDocument();
      expect(screen.getByText('結果 5')).toBeInTheDocument();
      expect(screen.queryByText('結果 6')).not.toBeInTheDocument();
    });
  });

  test('結果をクリックしてURLを開く', async () => {
    const mockOpen = jest.fn();
    window.open = mockOpen;
    
    mockOnSearch.mockResolvedValue(mockSearchResults);
    
    render(<SearchComponent onSearch={mockOnSearch} />);
    
    const input = screen.getByPlaceholderText('検索...');
    await userEvent.type(input, 'テスト');
    
    jest.advanceTimersByTime(300);
    
    await waitFor(() => {
      expect(screen.getByText('最初の結果')).toBeInTheDocument();
    });
    
    fireEvent.click(screen.getByText('最初の結果'));
    
    expect(mockOpen).toHaveBeenCalledWith('https://example.com/1', '_blank');
  });

  test('空のクエリで結果をクリアする', async () => {
    mockOnSearch.mockResolvedValue(mockSearchResults);
    
    render(<SearchComponent onSearch={mockOnSearch} />);
    
    const input = screen.getByPlaceholderText('検索...');
    
    // 最初に検索
    await userEvent.type(input, 'テスト');
    jest.advanceTimersByTime(300);
    
    await waitFor(() => {
      expect(screen.getByText('最初の結果')).toBeInTheDocument();
    });
    
    // 入力をクリア
    await userEvent.clear(input);
    jest.advanceTimersByTime(300);
    
    await waitFor(() => {
      expect(screen.queryByText('最初の結果')).not.toBeInTheDocument();
    });
  });
});

ベストプラクティス

1. 効果的なプロンプト

良い例:

typescript
// JWT認証ミドルウェアを作成
// - トークンの検証
// - ユーザー情報の抽出
// - エラーハンドリング
function authMiddleware

悪い例:

typescript
// 何かを作成
function something

2. コンテキストの提供

良い例:

typescript
// Express.jsアプリケーション用のユーザー認証ルート
// bcryptでパスワードハッシュ化
// JWTトークン生成
router.post('/login'

3. 段階的な生成

大きな機能は小さな部分に分けて生成:

  1. インターフェースの定義
  2. 基本的な実装
  3. エラーハンドリング
  4. テストの追加

4. コード品質の確保

  • 生成されたコードを常にレビュー
  • 型安全性を確認
  • セキュリティのベストプラクティスに従う
  • パフォーマンスを考慮

トラブルシューティング

よくある問題

  1. 不完全な生成

    • より具体的なプロンプトを提供
    • コンテキストを追加
  2. 型エラー

    • TypeScript設定を確認
    • 依存関係をインストール
  3. スタイルの不一致

    • プロジェクトの既存のコードスタイルを参照
    • ESLint/Prettierルールを適用

パフォーマンスの最適化

  • 大きなファイルでは段階的に生成
  • 不要な生成を無効化
  • キャッシュを活用

関連記事

究極の AI 駆動 IDE 学習ガイド