React搭配TypeScript使用教程及实战案例

13 阅读7分钟

一、React与TypeScript搭配核心优势

TypeScript(简称TS)是JavaScript的超集,核心优势是静态类型检查,能在开发阶段发现类型错误,避免运行时bug;同时提供更清晰的代码提示、更好的代码可维护性和可扩展性,尤其适合中大型React项目。

React与TS搭配的核心价值:

  • 组件Props类型约束:明确组件接收的参数类型、必填项,减少传参错误;
  • 状态(State)类型定义:规范状态的数据结构,避免状态赋值错误;
  • 减少类型相关注释:类型定义即文档,提升团队协作效率;
  • IDE友好提示:自动补全组件属性、方法,降低开发成本。

二、环境搭建(React + TS)

2.1 快速创建React+TS项目

使用create-react-app快速初始化,自带TS配置,无需手动配置webpack、tsconfig.json:

# 方式1:npx(推荐,无需全局安装)
npx create-react-app react-ts-demo --template typescript

# 方式2:yarn
yarn create react-app react-ts-demo --template typescript

项目创建完成后,核心文件说明:

  • .tsx:React组件文件后缀(包含JSX语法,必须用.tsx);
  • .ts:非组件的TS文件(如工具函数、类型定义);
  • tsconfig.json:TS的核心配置文件(指定编译选项、类型检查规则);
  • react-app-env.d.ts:React与TS的类型声明文件(自动生成,无需修改)。

2.2 核心配置(tsconfig.json关键项)

无需手动修改默认配置,重点了解以下关键项,便于后续自定义:

{
  "compilerOptions": {
    "target": "ESNext", // 目标JS版本
    "module": "ESNext", // 模块规范
    "jsx": "react-jsx", // 支持JSX语法(React 17+ 推荐)
    "strict": true, // 开启严格模式(推荐,强制类型检查)
    "esModuleInterop": true, // 兼容CommonJS模块
    "skipLibCheck": true, // 跳过第三方库类型检查
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true, // 支持导入JSON文件
    "isolatedModules": true,
    "noEmit": true // 不生成编译后的JS文件(由create-react-app处理)
  },
  "include": ["src"] // 需要编译的文件目录
}

三、React+TS基础使用(核心语法)

3.1 组件Props类型定义(最常用)

通过interface(接口)定义Props类型,明确组件接收的参数,支持必填/可选、默认值、联合类型等。

import React from 'react';

// 1. 定义Props接口(首字母大写,约定俗成)
interface UserCardProps {
  // 必填项(无?)
  name: string;
  age: number;
  // 可选项(加?)
  gender?: 'male' | 'female' | 'other'; // 联合类型,限制可选值
  // 函数类型(定义回调函数)
  onBtnClick: (id: number) => void;
}

// 2. 组件接收Props,指定类型为UserCardProps
const UserCard: React.FC<UserCardProps> = (props) => {
  // 解构Props(更简洁)
  const { name, age, gender = 'other', onBtnClick } = props;
  return (
    <div className="user-card">
      <h3>{name}</h3>
      <p>年龄:{age}</p>
      <p>性别:{gender}</p>
      <button onClick={() => onBtnClick(123)}>点击</button>
    </div>
  );
};

export default UserCard;

说明:React.FC 是React函数组件的类型,泛型参数即为Props的类型;可选参数通过?标记,可设置默认值避免undefined。

3.2 组件State类型定义

使用useState时,TS会自动推断状态类型(类型推导),复杂状态(如对象、数组)需手动指定类型。

import React, { useState } from 'react';

// 1. 简单状态(自动推断类型)
const SimpleState = () => {
  // TS自动推断count为number类型,setCount只能接收number
  const [count, setCount] = useState(0);
  // 错误示例:setCount('123') → 类型不匹配,开发阶段报错
  return <button onClick={() => setCount(count + 1)}>计数:{count}</button>;
};

// 2. 复杂状态(对象类型,手动指定)
interface UserState {
  name: string;
  age: number;
  isLogin: boolean;
}

const ComplexState = () => {
  // 手动指定状态类型为UserState,初始值需符合该类型
  const [user, setUser] = useState<UserState>({
    name: '张三',
    age: 20,
    isLogin: false,
  });

  // 修改状态(必须符合UserState类型)
  const login = () => {
    setUser({ ...user, isLogin: true });
  };

  return (
    <div>
      <p>{user.name}({user.age}岁)</p>
      <button onClick={login}>{user.isLogin ? '已登录' : '登录'}</button>
    </div>
  );
};

export default ComplexState;

3.3 事件处理类型定义

React事件有固定的TS类型(如点击事件React.MouseEvent、输入事件React.ChangeEvent),需指定事件类型和目标元素类型。

import React, { useState } from 'react';

const EventDemo = () => {
  const [inputValue, setInputValue] = useState('');

  // 1. 点击事件(React.MouseEvent,可指定目标元素类型)
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log('按钮点击', e.target.innerText);
  };

  // 2. 输入框变化事件(React.ChangeEvent,目标为输入框)
  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // e.target.value 自动推断为string类型
    setInputValue(e.target.value);
  };

  return (
    <div>
      <input
        type="text"
        value={inputValue}
        onChange={handleInputChange}
        placeholder="请输入内容"
      />
      <button onClick={handleClick}>提交</button>
    </div>
  );
};

export default EventDemo;

3.4 自定义Hook类型定义

自定义Hook返回值为多个数据时,需指定返回值类型(可通过元组、对象),确保使用时类型正确。

import React, { useState, useEffect } from 'react';

// 自定义Hook:获取窗口宽度
// 定义返回值类型(元组类型,固定顺序)
type UseWindowWidthReturn = [number, () => void];

const useWindowWidth = (): UseWindowWidthReturn => {
  const [width, setWidth] = useState(window.innerWidth);

  const updateWidth = () => {
    setWidth(window.innerWidth);
  };

  useEffect(() => {
    window.addEventListener('resize', updateWidth);
    return () => window.removeEventListener('resize', updateWidth);
  }, []);

  // 返回值必须符合UseWindowWidthReturn类型
  return [width, updateWidth];
};

// 使用自定义Hook
const WindowWidthDemo = () => {
  // 自动推断width为number,updateWidth为() => void
  const [width, updateWidth] = useWindowWidth();
  return <p>当前窗口宽度:{width}px</p>;
};

export default WindowWidthDemo;

四、React+TS实战案例(2个核心场景)

案例1:TodoList(基础综合案例)

涵盖Props、State、事件处理、数组类型,适合新手入门,完整实现“添加、删除、切换完成状态”功能。

import React, { useState } from 'react';

// 1. 定义Todo类型(单个任务)
interface Todo {
  id: number;
  text: string;
  done: boolean;
}

// 2. 定义TodoItem组件Props
interface TodoItemProps {
  todo: Todo;
  onToggle: (id: number) => void;
  onDelete: (id: number) => void;
}

// 3. 子组件:TodoItem
const TodoItem: React.FC<TodoItemProps> = ({ todo, onToggle, onDelete }) => {
  return (
    <li style={{ textDecoration: todo.done ? 'line-through' : 'none' }}>
      <input
        type="checkbox"
        checked={todo.done}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)} style={{ marginLeft: 10 }}>
        删除
      </button>
    </li>
  );
};

// 4. 父组件:TodoList
const TodoList: React.FC = () => {
  // 状态:todo列表(数组类型,元素为Todo)
  const [todos, setTodos] = useState<Todo[]>([
    { id: 1, text: '学习React+TS', done: false },
    { id: 2, text: '完成实战案例', done: true },
  ]);
  // 状态:输入框内容
  const [inputText, setInputText] = useState('');

  // 添加Todo
  const addTodo = (e: React.FormEvent) => {
    e.preventDefault(); // 阻止表单默认提交
    if (!inputText.trim()) return;
    const newTodo: Todo = {
      id: Date.now(), // 用时间戳作为唯一id
      text: inputText,
      done: false,
    };
    setTodos([...todos, newTodo]);
    setInputText(''); // 清空输入框
  };

  // 切换Todo完成状态
  const toggleTodo = (id: number) => {
    setTodos(todos.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo)));
  };

  // 删除Todo
  const deleteTodo = (id: number) => {
    setTodos(todos.filter((todo) => todo.id !== id));
  };

  return (
    <div style={{ maxWidth: 500, margin: '0 auto', padding: 20 }}>
      <h2>TodoList(React+TS)</h2>
      <form onSubmit={addTodo} style={{ marginBottom: 20 }}>
        <input
          type="text"
          value={inputText}
          onChange={(e) => setInputText(e.target.value)}
          placeholder="请输入任务"
          style={{ padding: 8, width: 300 }}
        />
        <button type="submit" style={{ padding: 8, marginLeft: 10 }}>
          添加
        </button>
      </form>
      <ul>
        {todos.map((todo) => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={toggleTodo}
            onDelete={deleteTodo}
          />
        ))}
      </ul>
    </div>
  );
};

export default TodoList;

案例2:用户列表(接口请求+复杂类型)

涵盖接口请求(fetch/axios)、加载状态、错误处理、复杂对象类型,贴近真实项目场景,使用axios请求接口(需先安装:npm install axios @types/axios)。

import React, { useState, useEffect } from 'react';
import axios from 'axios';

// 1. 定义用户类型(接口返回数据结构)
interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  phone: string;
  website: string;
}

// 2. 定义接口返回类型(假设接口返回{ data: User[] })
interface UserResponse {
  data: User[];
}

const UserList: React.FC = () => {
  // 状态:用户列表
  const [users, setUsers] = useState<User[]>([]);
  // 状态:加载状态
  const [loading, setLoading] = useState<boolean>(true);
  // 状态:错误信息
  const [error, setError] = useState<string | null>(null);

  // 接口请求( useEffect 模拟组件挂载时请求)
  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        // 调用接口(示例接口:JSONPlaceholder)
        const response = await axios.get<UserResponse>('https://jsonplaceholder.typicode.com/users');
        // 接口返回数据符合UserResponse类型,data为User数组
        setUsers(response.data.data);
        setError(null);
      } catch (err) {
        setError('请求用户列表失败,请稍后再试');
        console.error('请求错误:', err);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  // 加载中
  if (loading) return <div style={{ textAlign: 'center', padding: 50 }}>加载中...</div>;
  // 错误提示
  if (error) return <div style={{ textAlign: 'center', padding: 50, color: 'red' }}>{error}</div>;

  // 渲染用户列表
  return (
    <div style={{ maxWidth: 800, margin: '0 auto', padding: 20 }}>
      <h2>用户列表(React+TS+接口请求)</h2>
      <table border="1" style={{ width: '100%', borderCollapse: 'collapse', marginTop: 20 }}>
        <thead>
          <tr style={{ backgroundColor: '#f0f0f0' }}>
            <th style={{ padding: 10, textAlign: 'center' }}>ID</th>
            <th style={{ padding: 10, textAlign: 'center' }}>姓名</th>
            <th style={{ padding: 10, textAlign: 'center' }}>用户名</th>
            <th style={{ padding: 10, textAlign: 'center' }}>邮箱</th>
            <th style={{ padding: 10, textAlign: 'center' }}>电话</th>
            <th style={{ padding: 10, textAlign: 'center' }}>网站</th>
          </tr>
        </thead>
        <tbody>
          {users.map((user) => (
            <tr key={user.id}>
              <td style={{ padding: 10, textAlign: 'center' }}>{user.id}</td>
              <td style={{ padding: 10, textAlign: 'center' }}>{user.name}</td>
              <td style={{ padding: 10, textAlign: 'center' }}>{user.username}</td>
              <td style={{ padding: 10, textAlign: 'center' }}>{user.email}</td>
              <td style={{ padding: 10, textAlign: 'center' }}>{user.phone}</td>
              <td style={{ padding: 10, textAlign: 'center' }}>
                <a href={`http://${user.website}`} target="_blank" rel="noopener noreferrer">
                  {user.website}
                </a>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default UserList;

五、常见问题及解决方案

  • 问题1:“Property 'xxx' does not exist on type 'never'”? 解决方案:复杂状态(如数组、对象)初始值为空时,TS无法推断类型,需手动指定泛型(如useState<User[]>([]))。
  • 问题2:组件Props报错“Type 'undefined' is not assignable to type 'xxx'”? 解决方案:检查Props是否为必填项,若为可选项添加?,或给Props设置默认值。
  • 问题3:事件对象e报错“Property 'target' does not exist on type 'Event'”? 解决方案:指定事件类型(如React.MouseEvent<HTMLButtonElement>),明确目标元素类型。
  • 问题4:接口请求返回数据类型不匹配? 解决方案:定义接口返回类型(如案例2中的UserResponse),axios请求时指定泛型(axios.get<UserResponse>(url))。

六、总结

React+TS的核心是“类型约束”,重点掌握3个核心点:Props类型定义(interface)、State类型推导与手动指定、事件类型定义。通过基础语法练习和实战案例,能快速适应TS在React中的使用,尤其在中大型项目中,TS能显著提升代码质量和开发效率。

后续可深入学习:React组件泛型、Redux+TS、React Router+TS等进阶内容,进一步完善技术栈。