diff --git a/src/app/page.tsx b/src/app/page.tsx index f0c3033..9596927 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -19,6 +19,21 @@ type AppError = { message: string; }; +// 型ガード関数 +const isTodo = (obj: unknown): obj is Todo => { + return ( + obj !== null && + typeof obj === 'object' && + typeof (obj as any).id === 'number' && + typeof (obj as any).text === 'string' && + typeof (obj as any).completed === 'boolean' + ); +}; + +const isTodoArray = (data: unknown): data is Todo[] => { + return Array.isArray(data) && data.every(isTodo); +}; + export default function Home() { // Local Storage のキー const STORAGE_KEY = 'todo-app-data'; @@ -240,72 +255,66 @@ export default function Home() { } }, [todos, handleError]); + // 型安全なインポート機能 const importData = useCallback((event: React.ChangeEvent) => { const file = event.target.files?.[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (e) => { - try { - const importedData = JSON.parse(e.target?.result as string); - - if (!Array.isArray(importedData)) { - throw new Error('無効なデータ形式です'); - } - - const validTodos = importedData.filter(todo => - todo && - typeof todo.id === 'number' && - typeof todo.text === 'string' && - typeof todo.completed === 'boolean' - ); - - if (validTodos.length === 0) { - throw new Error('有効なTODOデータが見つかりません'); - } - - setTodos(validTodos); - event.target.value = ''; - } catch (error) { - handleError({ - type: 'IMPORT_ERROR', - message: 'ファイルの読み込みに失敗しました。正しいJSONファイルを選択してください。' - }); - event.target.value = ''; + if (!file) return; + + const reader = new FileReader(); + + reader.onload = (e: ProgressEvent) => { + try { + const result = e.target?.result; + if (typeof result !== 'string') { + throw new Error('ファイルの読み込みに失敗しました'); } - }; - - reader.onerror = () => { + + const parsedData: unknown = JSON.parse(result); + + if (!isTodoArray(parsedData)) { + throw new Error('無効なTODOデータ形式です'); + } + + setTodos(parsedData); + event.target.value = ''; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '不明なエラー'; handleError({ type: 'IMPORT_ERROR', - message: 'ファイルの読み込み中にエラーが発生しました' + message: `ファイルの読み込みに失敗しました: ${errorMessage}` }); event.target.value = ''; - }; - - reader.readAsText(file); - } + } + }; + + reader.onerror = () => { + handleError({ + type: 'IMPORT_ERROR', + message: 'ファイルの読み込み中にエラーが発生しました' + }); + event.target.value = ''; + }; + + reader.readAsText(file); }, [handleError]); - // アプリ起動時にデータを読み込み + // 型安全なLocal Storage読み込み useEffect(() => { const savedTodos = safeLocalStorageGet(STORAGE_KEY); if (savedTodos) { try { - const parsedTodos = JSON.parse(savedTodos); + const parsedData: unknown = JSON.parse(savedTodos); - if (Array.isArray(parsedTodos)) { - const validTodos = parsedTodos.filter(todo => - todo && - typeof todo.id === 'number' && - typeof todo.text === 'string' && - typeof todo.completed === 'boolean' - ); - setTodos(validTodos); + if (isTodoArray(parsedData)) { + setTodos(parsedData); + } else { + throw new Error('保存されたデータの形式が正しくありません'); } } catch (error) { + const errorMessage = error instanceof Error ? error.message : '不明なエラー'; handleError({ type: 'STORAGE_ERROR', - message: '保存されたデータの形式が正しくありません' + message: `データ読み込みエラー: ${errorMessage}` }); } }