型安全性を限界まで堅牢に

This commit is contained in:
2025-09-29 19:41:02 +09:00
parent f7d9950bbc
commit 253d25ab5c

View File

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