forked from semi-23e/nextjs-todo-tutorial
型安全性を限界まで堅牢に
This commit is contained in:
@@ -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,35 +255,33 @@ 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();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
|
||||||
|
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||||
try {
|
try {
|
||||||
const importedData = JSON.parse(e.target?.result as string);
|
const result = e.target?.result;
|
||||||
|
if (typeof result !== 'string') {
|
||||||
if (!Array.isArray(importedData)) {
|
throw new Error('ファイルの読み込みに失敗しました');
|
||||||
throw new Error('無効なデータ形式です');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const validTodos = importedData.filter(todo =>
|
const parsedData: unknown = JSON.parse(result);
|
||||||
todo &&
|
|
||||||
typeof todo.id === 'number' &&
|
|
||||||
typeof todo.text === 'string' &&
|
|
||||||
typeof todo.completed === 'boolean'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (validTodos.length === 0) {
|
if (!isTodoArray(parsedData)) {
|
||||||
throw new Error('有効なTODOデータが見つかりません');
|
throw new Error('無効なTODOデータ形式です');
|
||||||
}
|
}
|
||||||
|
|
||||||
setTodos(validTodos);
|
setTodos(parsedData);
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '不明なエラー';
|
||||||
handleError({
|
handleError({
|
||||||
type: 'IMPORT_ERROR',
|
type: 'IMPORT_ERROR',
|
||||||
message: 'ファイルの読み込みに失敗しました。正しいJSONファイルを選択してください。'
|
message: `ファイルの読み込みに失敗しました: ${errorMessage}`
|
||||||
});
|
});
|
||||||
event.target.value = '';
|
event.target.value = '';
|
||||||
}
|
}
|
||||||
@@ -283,29 +296,25 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
reader.readAsText(file);
|
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}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user