6、React + TypeScript

3 阅读3分钟

太棒了!👏 你已经掌握了 React 的核心架构,现在是时候让代码更健壮、可维护、专业了!


—— 类型安全的现代开发方式

💡 为什么大厂项目几乎都用 TypeScript?
因为它能在编码时就发现错误,提升代码质量、团队协作效率和重构信心。


一、TypeScript 能为 React 带来什么?✨

优势说明
🛡️ 类型检查提前发现 props 传错、方法调用错误等问题
💡 智能提示VSCode 自动补全组件属性、状态结构
🧩 文档即代码类型定义本身就是 API 文档
🔧 安全重构改名/删字段时,编辑器会帮你找到所有引用处
👥 团队协作统一接口规范,减少沟通成本

✅ 现实场景:

  • 不再因为拼错 userName 写成 username 导致白屏
  • 看到组件就知道它需要哪些 props,怎么用

二、基础配置(快速上手)

1. 创建支持 TS 的 React 项目

# 使用 Vite(推荐)
npm create vite@latest my-app -- --template react-ts

# 或使用 Create React App(旧)
npx create-react-app my-app --template typescript

文件后缀变为:.tsx(含 JSX) 和 .ts(纯逻辑)


三、给函数式组件添加类型 ✅

✅ 方式 1:使用 interface 定义 Props 类型

// components/UserCard.tsx
import { FC } from 'react';

interface User {
  name: string;
  age: number;
  email?: string; // 可选属性
}

interface UserCardProps {
  user: User;
  onRemove: (id: number) => void;
  userId: number;
}

const UserCard: FC<UserCardProps> = ({ user, onRemove, userId }) => {
  return (
    <div>
      <h3>{user.name}</h3>
      <p>年龄:{user.age}</p>
      {user.email && <p>邮箱:{user.email}</p>}
      <button onClick={() => onRemove(userId)}>删除</button>
    </div>
  );
};

export default UserCard;

🔍 FCFunctionComponent 的缩写,官方推荐但非必须。
现在更流行直接写函数类型:

const UserCard = ({ user, onRemove }: UserCardProps) => { ... }

✅ 方式 2:不使用 FC(现代推荐写法)

// 推荐:更简洁,避免 children 默认可选的问题
const UserCard = ({ user, onRemove }: UserCardProps) => {
  return (
    <div>
      <h3>{user.name}</h3>
      <button onClick={() => onRemove(user.id)}>删除</button>
    </div>
  );
};

四、常见类型的实战应用 💡

1. 处理事件类型

const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  console.log(e.currentTarget);
};

const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
  console.log(e.target.value);
};

<button onClick={handleClick}>点我</button>
<input onChange={handleInput} />
常见事件类型对应元素
React.ChangeEvent<HTMLInputElement><input>
React.MouseEvent<HTMLButtonElement><button>
React.FormEvent<HTMLFormElement><form onSubmit>

2. State 类型推断

const [users, setUsers] = useState<User[]>([]); // 用户列表

const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
// 字符串字面量联合类型,只能是这三个值之一

3. 使用泛型的自定义 Hook

// hooks/useLocalStorage.ts
import { useState, useEffect } from 'react';

function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
  const [value, setValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(error);
    }
  }, [key, value]);

  return [value, setValue];
}

export default useLocalStorage;

✅ 使用方式:

const [darkMode, setDarkMode] = useLocalStorage<boolean>('theme', false);
const [name, setName] = useLocalStorage<string>('name', '');

✨ 泛型让 Hook 更通用、类型更安全!


五、高级技巧与最佳实践 ✅

✅ 1. 类型守卫(Type Guards)

处理可能为空的情况:

function getUserAge(user: User | null): string {
  if (!user) return '未知';
  return `${user.age} 岁`;
}

或使用断言(谨慎):

const age = user!.age; // 明确告诉 TS:我保证 user 不为 null

✅ 2. Partial / Omit / Pick 工具类型

// 假设有完整用户类型
interface FullUser {
  id: number;
  name: string;
  email: string;
  password: string;
}

// 注册表单不需要 id 和 password
type RegisterForm = Omit<FullUser, 'id' | 'password'>;

// 编辑用户时部分字段可选
type UpdateUser = Partial<FullUser>;

✅ 3. 给 Context 添加类型

// context/AuthContext.tsx
import { createContext } from 'react';

interface AuthState {
  user: { name: string } | null;
  isLoggedIn: boolean;
}

interface AuthActions {
  login: (username: string) => void;
  logout: () => void;
}

type AuthContextType = AuthState & AuthActions;

export const AuthContext = createContext<AuthContextType | undefined>(undefined);

然后在 useContext 中使用:

const context = useContext(AuthContext);
if (!context) throw new Error('useAuth 必须在 AuthProvider 内使用');

return context;

六、避坑指南 ⚠️

❌ 错误 1:滥用 any

const data: any = await fetch('/api').then(res => res.json());
// ❌ 失去了类型保护意义

✅ 正确做法:定义接口

interface ApiResponse {
  users: { id: number; name: string }[];
  total: number;
}

const data = await fetch('/api').then(res => res.json()) as ApiResponse;

更好:使用 Zod 或 io-ts 做运行时校验(进阶)


❌ 错误 2:忽略 children 类型

interface ModalProps {
  isOpen: boolean;
  // 如果你希望支持 children
  children: React.ReactNode;
}

React.ReactNode 是最安全的选择,它可以是:

  • string, number, JSX.Element, null, undefined, 数组等

七、实战练习 🏋️‍♂️

✅ 练习 1:类型化 TodoList

将之前的 TodoList 用 TypeScript 重写:

  1. 定义 Todo 接口:
interface Todo {
  id: number;
  text: string;
  completed: boolean;
}
  1. TodoItem 组件添加 props 类型
  2. onToggleonDelete 回调添加函数类型

✅ 练习 2:构建类型安全的 useFetch

interface UseFetchResult<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

function useFetch<T>(url: string): UseFetchResult<T> {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch(url)
      .then(res => {
        if (!res.ok) throw new Error(`错误: ${res.status}`);
        return res.json();
      })
      .then(data => setData(data as T))
      .catch(err => setError(err.message))
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}

使用:

interface User {
  id: number;
  name: string;
}

const { data: users, loading } = useFetch<User[]>('/api/users');

✅ 总结:React + TypeScript 核心要点

概念要点
文件扩展名.tsx(含 JSX),.ts(逻辑)
Props 类型使用 interfacetype 定义
事件类型React.ChangeEvent, MouseEvent
泛型提升复用性和类型安全(如 useFetch<T>
工具类型Partial, Omit, Pick 提高灵活性
Context 类型使用 `createContext<Typeundefined>`
避免 any尽量用具体类型替代

🎯 下一步预告
你已经写出类型安全的专业级代码了!接下来我们要进入工程化阶段:

➡️ React 路由:React Router v6
—— 构建多页面 SPA 应用,实现页面跳转、动态路由、权限控制

是否继续?我将带你进入 第八课:React Router v6 实战