从 TodoList 看 React + TypeScript 类型实践

从 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 数组、onToggleonRemove 回调。
  • 泛型类型 Todo../types/todo 导入,描述了每个待办项的结构(idtitlecompleted)。
  • 函数组件通过解构 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:stringid: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 中定义。
  • addTodotodostoggleTodoremoveTodo 分别传递给子组件,类型自动匹配。

小结

通过这个 TodoList 项目,我们看到了 TypeScript 在 React 中的实际应用:

  • 为变量、数组、枚举、对象、函数参数等添加精确类型。
  • 使用 interfacetype 描述组件 Props。
  • 利用泛型(如 useState<T>getStorage<T>)保持类型安全。
  • 自定义 Hook 封装逻辑,并统一管理类型。

类型系统不仅减少了运行时错误,还提升了代码的可读性和维护性。希望这篇文章能帮助你更好地将 TypeScript 运用到 React 项目中。

相关推荐
风骏时光牛马1 小时前
JSON常见踩坑问题与实战避坑案例代码
前端
To_OC1 小时前
折腾两天 HTTP 接口调用,终于把 fetch 和前后端分离从书本概念落地到实操了
javascript·node.js·全栈
喵了几个咪1 小时前
基于 Flutter 的 Headless CMS 全平台前端架构:技术解析与二次开发导引
前端·flutter·架构
lantian1 小时前
TypeScript 模块系统核心原理:从ESM到CJS,彻底搞懂模块格式与解析逻辑
前端·typescript·ecmascript 6
Lear1 小时前
CSR、SSR、SSG 到底怎么选?一文讲透现代前端三大渲染模式
前端
এ慕ོ冬℘゜1 小时前
前端分页组件完整实现:样式 + 交互 + 逻辑全优化
前端·交互
Ajie'Blog1 小时前
Claude Opus 4.8 发布:Claude Code 能不能接住复杂项目
服务器·前端·javascript·人工智能·ai编程
2501_918126911 小时前
火柴人踢任意球
javascript·css·css3
San813_LDD1 小时前
[后端开发]GET/POST_带参/不带参
前端·后端·计算机网络·json