forked from semi-23e/nextjs-todo-tutorial
型安全性を限界まで堅牢に
This commit is contained in:
@@ -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<HTMLInputElement>) => {
|
||||
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 (!file) return;
|
||||
|
||||
if (!Array.isArray(importedData)) {
|
||||
throw new Error('無効なデータ形式です');
|
||||
}
|
||||
const reader = new FileReader();
|
||||
|
||||
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 = '';
|
||||
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||
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}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user