从 TodoList 看 React + TypeScript 类型实践
通过一个简单的 TodoList 项目,逐步理解 TypeScript 在 React 中的类型标注、接口定义、泛型以及自定义 Hook 的类型约束。
一、类型标注基础
TypeScript 为 JavaScript 添加了静态类型系统。我们可以为变量、数组、元组、枚举等添加类型注解。
typescript
let a:number = 1;
let b:string = a.toString();
console.log(typeof a);
console.log(typeof b);
let arr:number[] = [1,2,3];
let MyString:string[] = ['a','b','c','d'];
let array:[number,string,number] = [1,'1',2];
let arr2:Array<string> = ['a','b'];
number[]和Array<string>都表示数组。- 元组
[number, string, number]限定了固定长度和位置类型。
枚举(enum)提供一组命名常量:
typescript
enum Status{
Pending,
Fulfilled,
Rejected
}
let s:Status = Status.Pending;
s = Status.Fulfilled;
console.log(s);
console.log(Status[s]);
console.log(Status[0]);
类型注解只能在变量声明时使用一次:
typescript
let aa:any = 1;
aa= '11';
aa = {};
let bb:unknown = 1;
bb= 'b';
bb = {
name:'a',
age:18
}
// unknow 不能调用属性
// bb.name;
any 关闭类型检查,unknown 更安全,使用前需要类型收窄。
对象类型与接口:
typescript
let user2:{name:string,age:number} = {
name:'a',
age:18
}
interface User {
readonly id:number;
name:string;
age:number;
sex:'male'|'female';
email:string;
hobby?:string;
}
const u:User = {
id:1,
name:'a',
age:18,
sex:'male',
email:'a@qq.com'
}
接口支持只读属性(readonly)、可选属性(?)和字面量联合类型。
类型别名(type)可以联合多种类型:
typescript
type abc = string[]|number[];
let arr3:abc = ['1','2','3'];
let arr4:abc = arr3.map(item => parseInt(item));
console.log(arr4);
二、TodoList 组件:Props 类型定义
src/components/TodoList.tsx
typescript
import type { Todo } from "../types/todo"
interface Props {
todos: Todo[],
onToggle: (id: number) => void,
onRemove: (id: number) => void,
}
export default function TodoList({ todos, onToggle, onRemove }: Props) {
return (
<ul>
{
todos.map((todo: Todo) => {
return (
<li key={todo.id} style={{
listStyleType: 'none'
}}>
<button type="button" onClick={() => onToggle(todo.id)}>{todo.completed ? '√' : 'X'}</button>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>{todo.title}</span>
<button type="button" onClick={() => onRemove(todo.id)}>删除</button>
</li>
)
})
}
</ul>
)
}
- 使用
interface Props定义组件接收的属性:todos数组、onToggle和onRemove回调。 - 泛型类型
Todo从../types/todo导入,描述了每个待办项的结构(id、title、completed)。 - 函数组件通过解构
Props获取参数,并标注返回类型(TS 可自动推断)。
三、TodoInput 组件:状态与事件类型
src/components/TodoInput.tsx
typescript
import { useState } from "react";
interface Props {
onAdd:(title:string)=>void,
}
export default function TodoInput({onAdd}:Props){
const [title,setTitle] = useState('');
return(
<div>
<input type="text" placeholder="请输入内容" value={title} onChange={e=>setTitle(e.target.value)} />
<button type="button" onClick={()=>{
onAdd(title);
setTitle('');
}}>添加</button>
</div>
)
}
onAdd类型为(title: string) => void,接收字符串无返回值。useState被推断为string类型,无需显式注解。- 事件对象
e的类型由 TS 自动推断为React.ChangeEvent<HTMLInputElement>。
四、自定义 Hook useTodos:泛型与本地存储
src/hooks/useTodos.ts
typescript
import {
useState
} from 'react';
import type { Todo } from '../types/todo';
import { getStorage,setStorage } from '../utils/storages';
const STORAGE_KEY = 'todos';
export function useTodos(){
const [todos,setTodos] = useState<Todo[]>(()=>getStorage<Todo[]>(STORAGE_KEY,[]));
const addTodo = (title:string) =>{
const newTodo:Todo = {
id:+new Date(),
title,
completed:false
}
const newTodos = [...todos,newTodo];
setTodos(newTodos);
setStorage<Todo[]>(STORAGE_KEY,newTodos);
}
const toggleTodo = (id:number) =>{
const todo = todos.find(todo => todo.id === id);
if(todo) todo.completed = !todo.completed;
setTodos([...todos]);
setStorage<Todo[]>(STORAGE_KEY,todos);
}
const removeTodo = (id:number)=>{
const newTodos = todos.filter(todo => todo.id !== id);
setTodos(newTodos);
setStorage<Todo[]>(STORAGE_KEY,newTodos);
}
return (
{todos,
addTodo,
toggleTodo,
removeTodo}
)
}
useState<Todo[]>显式声明状态类型,并传入泛型给初始值函数getStorage<Todo[]>。- 每个方法都明确参数类型:
title:string、id:number。 setStorage<Todo[]>也使用泛型确保存储的数据类型一致。- 最后返回的对象包含状态和操作函数,类型由 TS 自动推导。
五、App 组件:组合使用
src/App.tsx
typescript
import TodoInput from './components/TodoInput';
import TodoList from './components/TodoList';
import { useTodos } from './hooks/useTodos';
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>
)
}
- 调用
useTodos获取状态和方法,所有类型都已在 Hook 中定义。 - 将
addTodo、todos、toggleTodo、removeTodo分别传递给子组件,类型自动匹配。
小结
通过这个 TodoList 项目,我们看到了 TypeScript 在 React 中的实际应用:
- 为变量、数组、枚举、对象、函数参数等添加精确类型。
- 使用
interface或type描述组件 Props。 - 利用泛型(如
useState<T>、getStorage<T>)保持类型安全。 - 自定义 Hook 封装逻辑,并统一管理类型。
类型系统不仅减少了运行时错误,还提升了代码的可读性和维护性。希望这篇文章能帮助你更好地将 TypeScript 运用到 React 项目中。