最近我用 React 和 TypeScript 从零写了一个 Todo 应用,整个过程让我深刻体会到:类型系统不是束缚,而是保护。它让组件之间的协作更清晰,状态管理更可靠,连 localStorage 的读写都变得安全可控。下面分享我是怎么一步步搭建这个小项目的。
核心思路:状态集中 + 类型约束
我把所有 todo 数据和操作逻辑封装在一个自定义 Hook useTodos 里,这样组件只需要“消费”状态和方法,不用关心实现细节。同时,用 TypeScript 接口明确约定数据结构,避免传错参数或访问不存在的属性。
首先定义 Todo 的结构:
// types/todo.ts
export interface Todo {
id: number;
title: string;
completed: boolean;
}
这个接口就像一份契约——任何地方使用 Todo 数据,都必须包含这三个字段,且类型固定。
自定义 Hook:useTodos
这是整个应用的核心。它用 useState 管理状态,并通过 useEffect 同步到 localStorage:
// hooks/useTodos.ts
export function useTodos() {
const [todos, setTodos] = useState<Todo[]>(() =>
getStorage<Todo[]>(STORAGE_KEY, [])
);
useEffect(() => {
setStorage<Todo[]>(STORAGE_KEY, todos);
}, [todos]);
const addTodo = (title: string) => {
const newTodo: Todo = {
id: +new Date(),
title,
completed: false
};
setTodos([...todos, newTodo]);
};
// toggleTodo 和 removeTodo 略...
return { todos, addTodo, toggleTodo, removeTodo };
}
注意这里用了泛型函数 getStorage<T> 和 setStorage<T>,确保读写 localStorage 时类型安全:
// utils/storages.ts
export function getStorage<T>(key: string, defaultValue: T): T {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
}
这样,即使从 localStorage 读出来的数据是字符串,TS 也能正确推断为 Todo[] 类型。
组件通信:靠 Props 接口对齐
父组件 App 只负责组合,不处理逻辑:
// App.tsx
export default function App() {
const { todos, addTodo, toggleTodo, removeTodo } = useTodos();
return (
<div>
<h1>TodoList</h1>
<TodoInput onAdd={addTodo} />
<TodoList todos={todos} onToggle={toggleTodo} onRemove={removeTodo} />
</div>
);
}
子组件通过 Props 接口明确声明自己需要什么:
// components/TodoList.tsx
interface Props {
todos: Todo[];
onToggle: (id: number) => void;
onRemove: (id: number) => void;
}
const TodoList: React.FC<Props> = ({ todos, onToggle, onRemove }) => {
return (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} onToggle={onToggle} onRemove={onRemove} />
))}
</ul>
);
};
这样,如果我在 App 里不小心传了错误类型的 onToggle,TypeScript 会立刻报错,而不是等到运行时才发现按钮点不动。
单个 Todo 项:细节处理
在 TodoItem 中,根据 completed 状态动态设置样式:
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title}
</span>
因为 todo 的类型是 Todo,所以 todo.completed 一定是布尔值,不会出现 undefined 导致样式异常。
输入框:防错处理
TodoInput 还做了空值校验:
const handleAdd = () => {
if (!value.trim()) return; // 防止添加空任务
onAdd(value);
setValue('');
};
配合 TS 的 string 类型,确保传给 onAdd 的一定是字符串,不会意外传入数字或 null。
总结:为什么值得用 TS?
- 提前暴露问题:写代码时就知道哪里传参错了;
- 自动文档:鼠标悬停就能看到函数签名和字段说明;
- 重构安全:改一个接口,所有用到的地方都会提示更新;
- 团队协作友好:别人看你的组件,一眼就知道要传什么。
这个 Todo 应用虽然简单,但用 TS 写完后,感觉整个项目“稳”了很多。以后再做大项目,我肯定会首选 TypeScript —— 它不是增加负担,而是帮我们写出更干净、更可靠的代码。