AIコード生成チュートリアル
Traeの高度なAIコード生成機能をマスターして、より速く効率的にコードを書きましょう。
概要
TraeのAIコード生成は、単純な自動補完を超えています。以下のことができます:
- 関数やクラス全体の生成
- 説明からコンポーネントを作成
- 完全な機能の構築
- テストの自動生成
- ドキュメントの作成
- 既存コードのリファクタリング
はじめに
コード生成の有効化
- 設定を開く:
Ctrl+,(Windows/Linux) またはCmd+,(Mac) - 移動: AI > コード生成
- 機能を有効化:
- ✅ 自動補完
- ✅ 関数生成
- ✅ コンポーネント足場
- ✅ テスト生成
コード生成のトリガー
方法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 something2. コンテキストの提供
良い例:
typescript
// Express.jsアプリケーション用のユーザー認証ルート
// bcryptでパスワードハッシュ化
// JWTトークン生成
router.post('/login'3. 段階的な生成
大きな機能は小さな部分に分けて生成:
- インターフェースの定義
- 基本的な実装
- エラーハンドリング
- テストの追加
4. コード品質の確保
- 生成されたコードを常にレビュー
- 型安全性を確認
- セキュリティのベストプラクティスに従う
- パフォーマンスを考慮
トラブルシューティング
よくある問題
不完全な生成
- より具体的なプロンプトを提供
- コンテキストを追加
型エラー
- TypeScript設定を確認
- 依存関係をインストール
スタイルの不一致
- プロジェクトの既存のコードスタイルを参照
- ESLint/Prettierルールを適用
パフォーマンスの最適化
- 大きなファイルでは段階的に生成
- 不要な生成を無効化
- キャッシュを活用