正在加载,请稍候…

TypeScript + React 最佳实践:组件、Props 和 Hooks 的类型化

编写更好的 TypeScript React 代码:使用接口类型化 props、泛型组件、类型化 hooks、用可辨识联合处理状态,以及避免常见 TS 错误。

TypeScript + React 最佳实践:组件、Props 和 Hooks 的类型化

在 React 项目中设置 TypeScript

# 使用 Vite 创建新项目(2026 年推荐)
npm create vite@latest my-app -- --template react-ts

# 为现有 React 项目添加 TypeScript
npm install -D typescript @types/react @types/react-dom
npx tsc --init

TypeScript + React 最佳实践:组件、Props 和 Hooks 的类型化插图

类型化组件 Props

Interface 与 type alias

// Interface — 推荐用于 props(可扩展,错误信息更清晰)
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
  children?: React.ReactNode;
}

// Type alias — 适用于联合类型和计算类型
type ButtonVariant = 'primary' | 'secondary' | 'danger';

函数组件

// 现代方式 — 不要使用 React.FC(已在 React 18 中弃用)
function Button({ label, onClick, variant = 'primary', disabled = false }: ButtonProps) {
  return (
    <button
      className={clsx('btn', `btn-${variant}`)}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

// 箭头函数风格
const Button = ({ label, onClick }: ButtonProps) => (
  <button onClick={onClick}>{label}</button>
);

// 导出类型
export type { ButtonProps };
export { Button };

扩展 HTML 元素 props

// 扩展原生 HTML props — 用户可以传递任何有效的 button 属性
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  label: string;
  variant?: 'primary' | 'secondary';
  isLoading?: boolean;
}

function Button({ label, variant = 'primary', isLoading, ...rest }: ButtonProps) {
  return (
    <button
      className={clsx('btn', `btn-${variant}`, { loading: isLoading })}
      disabled={isLoading || rest.disabled}
      {...rest}  // 展开 onClick, type, form 等
    >
      {isLoading ? <Spinner /> : label}
    </button>
  );
}

Children 类型

interface CardProps {
  children: React.ReactNode;     // 最宽松:React 可渲染的任何内容
  header?: React.ReactElement;   // 必须是 React 元素(不是字符串/数字)
  footer?: string | React.ReactElement; // 字符串或元素
}

// 对于只接受特定子组件的组件
interface ListProps {
  children: React.ReactElement<ListItemProps> | React.ReactElement<ListItemProps>[];
}

TypeScript + React 最佳实践:组件、Props 和 Hooks 的类型化插图

组件状态的可辨识联合

// ❌ 不清晰:哪些组合是有效的?
interface DataViewProps {
  status: 'idle' | 'loading' | 'success' | 'error';
  data?: User[];
  error?: Error;
}

// ✅ 清晰:每个状态都是明确的
type DataViewProps =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User[] }
  | { status: 'error'; error: Error };

function DataView(props: DataViewProps) {
  if (props.status === 'idle') return <EmptyState />;
  if (props.status === 'loading') return <Spinner />;
  if (props.status === 'error') return <ErrorMessage error={props.error} />;
  // TypeScript 知道此处 status 为 'success' 且 data: User[] 存在
  return <UserList users={props.data} />;
}

类型化 Hooks

useState

// TypeScript 从初始值推断类型
const [count, setCount] = useState(0);           // number
const [name, setName] = useState('');             // string

// 初始值为 null/undefined 时显式指定类型
const [user, setUser] = useState<User | null>(null);
const [items, setItems] = useState<Item[]>([]);

// 复杂状态
interface FormState {
  name: string;
  email: string;
  errors: Record<string, string>;
}

const [form, setForm] = useState<FormState>({
  name: '',
  email: '',
  errors: {},
});

useRef

// DOM 元素 ref
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);

// 可变值 ref(无 null)
const timerRef = useRef<ReturnType<typeof setTimeout>>();
const countRef = useRef<number>(0);

// 使用
useEffect(() => {
  inputRef.current?.focus(); // 可选链,因为 DOM ref 初始为 null
}, []);

useReducer

type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' }
  | { type: 'SET'; payload: number }
  | { type: 'RESET' };

interface CounterState {
  count: number;
  history: number[];
}

function reducer(state: CounterState, action: Action): CounterState {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1, history: [...state.history, state.count] };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    case 'SET':
      return { ...state, count: action.payload }; // TypeScript 知道 payload 存在
    case 'RESET':
      return { count: 0, history: [] };
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0, history: [] });

  return (
    <div>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <span>{state.count}</span>
      <button onClick={() => dispatch({ type: 'SET', payload: 10 })}>Set 10</button>
    </div>
  );
}

TypeScript + React 最佳实践:组件、Props 和 Hooks 的类型化插图

自定义 Hooks

// 返回元组 — 使用 'as const' 类型
function useToggle(initial = false): [boolean, () => void] {
  const [value, setValue] = useState(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  return [value, toggle];
}

// 返回对象
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const controller = new AbortController();
    setLoading(true);

    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json() as Promise<T>;
      })
      .then(data => {
        setData(data);
        setLoading(false);
      })
      .catch(err => {
        if (err.name !== 'AbortError') {
          setError(err);
          setLoading(false);
        }
      });

    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
}

// 使用 — TypeScript 推断 User[] 类型
const { data, loading, error } = useFetch<User[]>('/api/users');

泛型组件

// 适用于任何类型的列表组件
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 = 'No items' }: ListProps<T>) {
  if (items.length === 0) return <div className="empty-state">{emptyMessage}</div>;

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

// 使用 TypeScript 推断
interface User {
  id: string;
  name: string;
  email: string;
}

function UserList({ users }: { users: User[] }) {
  return (
    <List<User>
      items={users}
      keyExtractor={(user) => user.id}
      renderItem={(user) => <span>{user.name} — {user.email}</span>}
      emptyMessage="No users found"
    />
  );
}

事件处理器类型

// 常见事件处理器类型
function Form() {
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
  };

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

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

  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') handleSubmit(e as any);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} onKeyDown={handleKeyDown} />
      <button onClick={handleClick}>Submit</button>
    </form>
  );
}

Context 与 TypeScript

interface ThemeContextValue {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

// 创建带有合理默认值或 undefined 的 context
const ThemeContext = React.createContext<ThemeContextValue | undefined>(undefined);

// 带空值检查的自定义 hook
function useTheme(): ThemeContextValue {
  const context = React.useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used inside ThemeProvider');
  }
  return context;
}

function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  const toggleTheme = useCallback(() => {
    setTheme(t => (t === 'light' ? 'dark' : 'light'));
  }, []);

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

React 中常见的 TypeScript 错误(及修复)

错误 原因 修复
Type 'string' is not assignable to type 'never' 可辨识联合不匹配 添加适当的类型守卫
Object is possibly 'null' useRef 或条件值 可选链 ?. 或空值检查
Property 'X' does not exist on type 'Y' 使用了错误的接口 检查 prop 类型或扩展接口
Cannot invoke an object which is possibly 'undefined' 可选的函数 prop 使用 onClick?.() 守卫
JSX element type does not have any construct signatures 将组件作为 prop 传递但类型不正确 使用 React.ComponentType<Props>

快速参考

// Children
children: React.ReactNode           // 任何可渲染内容
children: React.ReactElement        // 必须是 JSX 元素
children: React.PropsWithChildren<Props> // 向 Props 添加 children

// Refs
ref: React.RefObject<HTMLDivElement>  // 只读
ref: React.MutableRefObject<number>  // 可变

// Events
onClick: React.MouseEventHandler<HTMLButtonElement>
onChange: React.ChangeEventHandler<HTMLInputElement>
onSubmit: React.FormEventHandler<HTMLFormElement>

// 组件类型
type FC = (props: Props) => React.ReactElement | null
ComponentType<Props>  // 类组件或函数组件
ElementType           // 字符串标签或组件

总结

2026 年强类型 TypeScript + React 实践:

  1. 使用 interfaces 定义 props — 更易扩展,错误信息更清晰
  2. 使用可辨识联合处理组件状态 — 消除不可能的状态
  3. 在 TypeScript 无法推断时显式类型化 hooks(useState 与 null,useRef)
  4. 泛型组件用于可复用的列表、表格和表单字段
  5. 扩展 HTML 属性使自定义组件仍能接受原生 props
  6. useContext 配合自定义 hooks — 在 hook 中抛出错误,而非每个调用点
  7. 避免使用 React.FC — 已在 React 18 中弃用,直接类型化 props

→ 在 JSON ViewerURL Parser 中使用真实 TypeScript 工具进行练习。