React 项目中如何使用 TypeScript?有哪些最佳实践?

一、是什么

TypeScript 是 JavaScript 的超集,为 React 开发提供了静态类型检查更好的 IDE 支持更强的重构信心。在 React 项目中使用 TypeScript,可以在编译期捕获 props 传递错误、state 类型不匹配、事件处理类型问题等常见 bug,显著提升代码质量和开发体验。

React 本身使用 TypeScript 编写了完整的类型定义(@types/react),对 TypeScript 有一流的支持。

二、组件类型定义

函数组件的两种写法

// 方式一:直接标注 props 参数类型(推荐)
interface UserCardProps {
  name: string;
  age: number;
  avatar?: string;
  onFollow: (userId: string) => void;
}

function UserCard({ name, age, avatar, onFollow }: UserCardProps) {
  return (
    <div className="user-card">
      {avatar && <img src={avatar} alt={name} />}
      <h3>{name}</h3>
      <span>{age} 岁</span>
      <button onClick={() => onFollow(name)}>关注</button>
    </div>
  );
}

// 方式二:使用 React.FC(不推荐)
const UserCard: React.FC<UserCardProps> = ({ name, age }) => {
  return <div>{name}</div>;
};

为什么不推荐 React.FC

  • 早期版本隐式包含 children 类型,React 18 已移除但造成过混淆
  • 不支持泛型组件
  • 对默认 props 的类型推断不理想
  • 函数声明方式更直接,类型推断更清晰

interface vs type

// interface:可扩展,适合 props 定义
interface ButtonProps {
  variant: 'primary' | 'secondary';
  size: 'sm' | 'md' | 'lg';
}

interface IconButtonProps extends ButtonProps {
  icon: React.ReactNode;
}

// type:适合联合类型、交叉类型、工具类型
type Status = 'idle' | 'loading' | 'success' | 'error';

type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

一般建议:组件 props 用 interface,其他类型定义视场景选择。保持项目内一致即可。

三、Hooks 类型标注

useState

// 简单类型:自动推断
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string

// 复杂类型或初始值为 null:需要显式标注
interface User {
  id: string;
  name: string;
  email: string;
}

const [user, setUser] = useState<User | null>(null);

// 联合类型
type Theme = 'light' | 'dark' | 'system';
const [theme, setTheme] = useState<Theme>('system');

useRef

// DOM 引用:初始值为 null
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current?.focus();

// 可变值容器:初始值非 null
const timerRef = useRef<number>(0);

useRef<T>(null) 返回 RefObject<T>.current 只读),useRef<T>(initialValue) 返回 MutableRefObject<T>.current 可写)。

useReducer

type TodoAction =
  | { type: 'ADD_TODO'; payload: string }
  | { type: 'TOGGLE_TODO'; payload: number }
  | { type: 'SET_FILTER'; payload: 'all' | 'active' | 'completed' };

function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case 'ADD_TODO':
      return { ...state, todos: [...state.todos, { id: Date.now(), text: action.payload, done: false }] };
    case 'TOGGLE_TODO':
      return { ...state, todos: state.todos.map((t) => t.id === action.payload ? { ...t, done: !t.done } : t) };
    case 'SET_FILTER':
      return { ...state, filter: action.payload };
  }
}

const [state, dispatch] = useReducer(todoReducer, { todos: [], filter: 'all' });

使用判别联合类型(Discriminated Union)定义 action,TypeScript 可以在 switch-case 中自动收窄 payload 的类型。

四、事件类型

React 事件都有对应的泛型类型,需要指定目标元素类型:

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

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
};

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

常用事件类型速查:

事件处理器类型
onChange (input)React.ChangeEvent<HTMLInputElement>
onClickReact.MouseEvent<HTMLButtonElement>
onSubmitReact.FormEvent<HTMLFormElement>
onKeyDownReact.KeyboardEvent<HTMLInputElement>
onFocus/onBlurReact.FocusEvent<HTMLInputElement>
onDragReact.DragEvent<HTMLDivElement>

五、Context 类型

interface AuthContextType {
  user: User | null;
  login: (credentials: Credentials) => Promise<void>;
  logout: () => void;
  isLoading: boolean;
}

const AuthContext = React.createContext<AuthContextType | null>(null);

function useAuth(): AuthContextType {
  const context = React.useContext(AuthContext);
  if (context === null) {
    throw new Error('useAuth 必须在 AuthProvider 内使用');
  }
  return context;
}

function AuthProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  const login = async (credentials: Credentials) => {
    setIsLoading(true);
    const user = await authAPI.login(credentials);
    setUser(user);
    setIsLoading(false);
  };

  const logout = () => {
    authAPI.logout();
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, login, logout, isLoading }}>
      {children}
    </AuthContext.Provider>
  );
}

将 Context 默认值设为 null 并在自定义 hook 中进行空值检查,比提供一个假的默认值更安全。

六、泛型组件

interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string;
  emptyMessage?: string;
}

function List<T>({ items, renderItem, keyExtractor, emptyMessage }: ListProps<T>) {
  if (items.length === 0) {
    return <p>{emptyMessage || '暂无数据'}</p>;
  }

  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>{renderItem(item, index)}</li>
      ))}
    </ul>
  );
}

// 使用时自动推断泛型
<List
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

泛型组件也可以结合判别联合 Props,按 type 字段区分不同 props 形态,TypeScript 会自动收窄每个分支的类型。

七、实用工具类型

import type { ComponentProps, PropsWithChildren, HTMLAttributes } from 'react';

// 提取已有组件的 props 类型
type ButtonNativeProps = ComponentProps<'button'>;
type InputProps = ComponentProps<'input'>;
type MyComponentProps = ComponentProps<typeof MyComponent>;

// 为组件添加 children
type CardProps = PropsWithChildren<{
  title: string;
  bordered?: boolean;
}>;

// 扩展原生 HTML 属性
interface CustomInputProps extends HTMLAttributes<HTMLDivElement> {
  label: string;
  error?: string;
}

// Omit 排除不需要的属性
interface TextFieldProps extends Omit<ComponentProps<'input'>, 'size'> {
  size: 'sm' | 'md' | 'lg';
  label: string;
}

// Record 定义映射类型
const sizeMap: Record<'sm' | 'md' | 'lg', number> = {
  sm: 12,
  md: 16,
  lg: 20,
};

八、常见陷阱与最佳实践

避免 any,善用 unknown

// 差:跳过类型检查
function parseData(data: any) {
  return data.name; // 无类型保护
}

// 好:强制进行类型检查
function parseData(data: unknown) {
  if (typeof data === 'object' && data !== null && 'name' in data) {
    return (data as { name: string }).name;
  }
  throw new Error('Invalid data');
}

类型导入使用 type 关键字

// 使用 type-only import,编译时完全移除
import type { User, Credentials } from './types';
import type { FC, ReactNode } from 'react';

as const 与字面量类型

const ROUTES = {
  HOME: '/',
  ABOUT: '/about',
  USER: '/user/:id',
} as const;

// typeof ROUTES[keyof typeof ROUTES] = '/' | '/about' | '/user/:id'
type Route = (typeof ROUTES)[keyof typeof ROUTES];

九、总结

场景推荐做法
组件 Propsinterface + 参数解构标注
组件写法普通函数声明,不用 React.FC
useState简单值自动推断,复杂值 useState<T>
useRefDOM 用 useRef<HTMLElement>(null),可变值给初始值
事件React.ChangeEvent<HTMLInputElement>
ContextcreateContext<T | null>(null) + 非空断言 hook
泛型组件function Comp<T>(props: Props<T>)
导入类型始终使用 import type
避免 anyunknown 替代,配合类型守卫