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

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;
};
// 型ガード関数
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,35 +255,33 @@ export default function Home() {
}
}, [todos, handleError]);
// 型安全なインポート機能
const importData = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
reader.onload = (e: ProgressEvent<FileReader>) => {
try {
const importedData = JSON.parse(e.target?.result as string);
if (!Array.isArray(importedData)) {
throw new Error('無効なデータ形式です');
const result = e.target?.result;
if (typeof result !== 'string') {
throw new Error('ファイルの読み込みに失敗しました');
}
const validTodos = importedData.filter(todo =>
todo &&
typeof todo.id === 'number' &&
typeof todo.text === 'string' &&
typeof todo.completed === 'boolean'
);
const parsedData: unknown = JSON.parse(result);
if (validTodos.length === 0) {
throw new Error('効なTODOデータが見つかりません');
if (!isTodoArray(parsedData)) {
throw new Error('効なTODOデータ形式です');
}
setTodos(validTodos);
setTodos(parsedData);
event.target.value = '';
} catch (error) {
const errorMessage = error instanceof Error ? error.message : '不明なエラー';
handleError({
type: 'IMPORT_ERROR',
message: 'ファイルの読み込みに失敗しました。正しいJSONファイルを選択してください。'
message: `ファイルの読み込みに失敗しました: ${errorMessage}`
});
event.target.value = '';
}
@@ -283,29 +296,25 @@ export default function Home() {
};
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}`
});
}
}