React TypeScript 使用指南

March 27, 2025

概述

本教程旨在帮助开发者掌握在 React 项目中使用 TypeScript 的核心概念和高级技巧。从基础入门到复杂模式,我们将系统地探索如何利用 TypeScript 的强大类型系统来显著提升 React 应用程序的可维护性、可读性和健壮性。

目录

  1. TypeScript 与 React 基础
  2. 组件类型定义
  3. React Hooks 与 TypeScript
  4. 高级属性类型
  5. 泛型在 React 中的应用
  6. 高级 Hooks 模式
  7. TypeScript 类型深入探讨
  8. 高级设计模式
  9. 与外部库集成

1. TypeScript 与 React 基础

React 在 TypeScript 中的工作原理

TypeScript 为 React 开发提供了强大的类型检查和智能提示功能,这是提高代码质量和开发效率的关键因素。要在 React 项目中充分利用 TypeScript,你需要了解几个核心配置和概念:

  • tsconfig.json 配置jsx: "preserve""react" 选项决定了 JSX 代码如何被转换。preserve 保留 JSX 语法供后续转换工具处理,而 react 则直接将 JSX 转换为 React.createElement 调用。

  • @types/react 类型定义:这个包提供了 React 库的类型声明,是 TypeScript 与 React 协同工作的基础。它定义了组件、hooks、事件和 JSX 元素的类型,使编译器能够理解 React 特有的语法和模式。

  • JSX 类型系统:TypeScript 能够智能地推断和检查 JSX 元素的属性类型,提供实时错误检测和代码补全功能。

在 TypeScript 编译过程中,JSX 语法被转换为 React.createElement 函数调用,编译器会验证传递给这些函数的参数类型是否正确。这种类型检查机制能够在编译时捕获许多常见错误,而不是等到运行时才发现问题。

在现代框架中的应用

现代 React 生态系统中的主流框架如 Next.js、Remix 和 Gatsby 都提供了一流的 TypeScript 支持,大大简化了开发环境配置过程。这些框架的优势在于:

  • 零配置支持:大多数框架提供开箱即用的 TypeScript 配置,无需手动设置复杂的编译选项。

  • 专用类型定义:这些框架提供了特定于框架功能的类型定义,如 Next.js 的页面路由类型、Gatsby 的 GraphQL 查询类型等。

  • 增强的开发体验:与编辑器集成提供实时类型检查、智能代码补全和文档提示,显著提高开发效率。

  • 渐进式采用:这些框架允许你逐步引入 TypeScript,可以从关键组件开始,然后逐渐扩展到整个应用程序。

这种深度集成使得在现代 React 应用中采用 TypeScript 变得更加自然和高效。

2. 组件类型定义

组件属性类型

在 React 中定义组件时,我们需要为组件的 props 指定类型:

interface ButtonProps {
  className: string;
  onClick?: () => void;
}

export const Button = (props: ButtonProps) => {
  return <button className={props.className} onClick={props.onClick}></button>;
};

函数组件类型

React 提供了 React.FCReact.FunctionComponent 类型,但现代 React 开发中更推荐直接为 props 定义类型:

// 不使用 React.FC
const Button = (props: ButtonProps) => {
  return <button className={props.className}></button>;
};

// 使用 React.FC (不推荐)
const Button: React.FC<ButtonProps> = (props) => {
  return <button className={props.className}></button>;
};

子组件类型

处理 children 属性时,我们可以使用 React.ReactNode 类型:

interface ContainerProps {
  children: React.ReactNode;
}

export const Container = (props: ContainerProps) => {
  return <div className="container">{props.children}</div>;
};

事件处理函数类型

React 提供了丰富的事件类型定义:

interface ButtonProps {
  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}

export const Button = (props: ButtonProps) => {
  return <button onClick={props.onClick}>Click me</button>;
};

使用 HTML 属性

React 提供了内置的 HTML 元素属性类型,可以通过 React.ComponentProps 或特定元素的属性类型来使用:

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "primary" | "secondary";
}

export const Button = ({ variant = "primary", ...rest }: ButtonProps) => {
  return <button className={`btn-${variant}`} {...rest} />;
};

3. React Hooks 与 TypeScript

useState

使用 useState 时,TypeScript 通常能够推断状态类型,但有时需要显式指定:

type Tag = {
  id: number;
  value: string;
};

const [tags, setTags] = useState<Tag[]>([]);

useState 与 undefined

当状态可能为 undefined 时,需要明确指定类型:

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

useEffect

useEffect 的类型主要关注依赖数组和清理函数:

useEffect(() => {
  const handler = () => {
    console.log("Window resized");
  };

  window.addEventListener("resize", handler);

  return () => {
    window.removeEventListener("resize", handler);
  };
}, []);

useCallback 和 useMemo

这些 hooks 的类型定义需要关注返回值类型和依赖数组:

const handleClick = useCallback((id: string) => {
  console.log(id);
}, []);

const expensiveValue = useMemo<ComplexType>(() => {
  return computeExpensiveValue(a, b);
}, [a, b]);

useRef

useRef 需要指定引用值的类型,特别是在处理 DOM 元素时:

// 基本用法
const count = useRef<number>(0);

// DOM 元素引用
const inputRef = useRef<HTMLInputElement>(null);

useReducer

useReducer 需要为状态和动作定义清晰的类型:

type State = { count: number };
type Action =
  | { type: "increment"; payload: number }
  | { type: "decrement"; payload: number };

const reducer = (state: State, action: Action): State => {
  switch (action.type) {
    case "increment":
      return { count: state.count + action.payload };
    case "decrement":
      return { count: state.count - action.payload };
  }
};

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

4. 高级属性类型

可辨识联合类型

可辨识联合类型(Discriminated Unions)是处理组件具有多种变体的强大方式:

type ButtonProps =
  | { variant: "primary"; onClick: () => void }
  | { variant: "secondary"; href: string };

export const Button = (props: ButtonProps) => {
  if (props.variant === "primary") {
    return <button onClick={props.onClick}>Click me</button>;
  } else {
    return <a href={props.href}>Link</a>;
  }
};

条件属性

有时我们需要根据某个属性的存在与否来决定其他属性是否必须:

type FormProps =
  | { showSubmit: true; onSubmit: () => void }
  | { showSubmit: false };

组件与节点传递

区分传递 React 组件和 React 节点的不同方式:

// 传递组件
type ComponentProps = {
  component: React.ComponentType<{ name: string }>;
};

// 传递节点
type NodeProps = {
  children: React.ReactNode;
};

使用 classnames 的变体

结合 classnames 库处理组件样式变体:

type ButtonProps = {
  variant: "primary" | "secondary";
  size: "small" | "medium" | "large";
};

export const Button = ({ variant, size, ...rest }: ButtonProps) => {
  return (
    <button
      className={classnames({
        [`btn-${variant}`]: true,
        [`btn-${size}`]: true,
      })}
      {...rest}
    />
  );
};

部分自动完成

创建既有类型安全又能提供良好开发体验的 API 是 TypeScript 中的一项关键技术。下面的示例展示了如何实现这一平衡:

const colors = {
  primary: "blue",
  secondary: "gray",
  success: "green",
  danger: "red",
} as const;

type ColorProps = {
  color: keyof typeof colors | (string & {});
};

关键之处

  • as const 断言将对象转换为只读类型,使 TypeScript 将属性值视为字面量类型而非一般的字符串类型。

  • keyof typeof colors 创建了一个字符串字面量联合类型:"primary" | "secondary" | "success" | "danger",提供精确的自动完成。

  • (string & {}) 类型允许用户传入预定义选项之外的自定义字符串值,同时保持类型安全。

  • 这种模式特别适用于创建组件 API,既能提供有用的自动完成建议,又不会过度限制用户的选择。

as const 断言

as const 断言是 TypeScript 中一个强大的特性,它允许你创建精确的只读类型定义。这在 React 组件开发中特别有用:

const variants = ["primary", "secondary", "success"] as const;
type VariantType = typeof variants[number]; // "primary" | "secondary" | "success"

关键之处

  • 没有 as const 时,TypeScript 会将数组推断为 string[] 类型,丢失具体的字面量值信息。

  • 添加 as const 后,数组被推断为 readonly ["primary", "secondary", "success"] 这样的只读元组类型。

  • typeof variants[number] 是一个索引访问类型,它从元组中提取所有可能的值,创建字符串字面量联合类型。

  • 这种模式特别适合定义组件变体、主题选项或任何有限集合的选择项,既保证了类型安全,又简化了维护工作。当你添加新的变体时,TypeScript 会自动更新联合类型。

5. 泛型在 React 中的应用

类型助手

类型助手(Type Helpers)是 TypeScript 中强大的工具,它们允许你创建可复用的类型转换逻辑,大大提高代码的可维护性:

type ExtractProps<T> = T extends React.ComponentType<infer P> ? P : never;

type ButtonProps = ExtractProps<typeof Button>;

关键之处

  • 类型助手使用泛型和条件类型来实现类型级别的函数功能。

  • infer P 是一个强大的类型推断机制,它能从复杂类型中提取出我们需要的部分。

  • 上面的 ExtractProps 类型助手可以从任何 React 组件类型中提取出其 props 类型,无需手动复制接口定义。

  • 这种模式特别适合处理第三方组件或复杂组件层次结构,可以显著减少类型定义的重复工作。

  • 当组件的 props 类型发生变化时,使用类型助手提取的类型会自动更新,保持类型定义的同步性。

带约束的类型助手

泛型约束是提高类型助手安全性和可预测性的重要机制。它们允许你限制泛型参数必须满足特定条件:

type ExtractPropsWithChildren<T extends React.ComponentType<any>> =
  T extends React.ComponentType<infer P> ? P : never;

关键之处

  • T extends React.ComponentType<any> 确保传入的类型必须是 React 组件类型,这样可以在编译时捕获错误使用。

  • 泛型约束提供了类型安全的边界,防止类型助手被应用于不兼容的类型。

  • 约束还能改善 IDE 中的类型提示和文档,使开发者更容易理解如何正确使用类型助手。

  • 在复杂的类型系统中,适当的约束可以简化类型推断过程,提高编译性能。

  • 这种模式在构建大型组件库或框架时特别有价值,它能确保类型系统的一致性和可靠性。

泛型本地存储钩子

自定义 hooks 是 React 中复用逻辑的强大方式,结合 TypeScript 的泛型,我们可以创建类型安全且灵活的工具:

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

  // 省略实现细节...

  return [storedValue, setValue] as const;
}

关键之处

  • 泛型参数 <T> 使这个 hook 能够处理任何类型的数据,同时保持完整的类型安全性。

  • 初始化函数中的类型处理确保了从 localStorage 读取的数据会被正确地转换为预期类型。

  • as const 断言创建了一个元组类型的返回值,而不是更宽松的数组类型,这保证了返回值的结构稳定性。

  • 使用这个 hook 时,TypeScript 能够正确推断存储值的类型,提供精确的类型检查和自动完成:

    // 推断为 [string, (value: string | ((val: string) => string)) => void]
    const [name, setName] = useLocalStorage("name", "");
    
    // 推断为 [User | null, (value: User | null | ((val: User | null) => User | null)) => void]
    const [user, setUser] = useLocalStorage<User | null>("user", null);
    
  • 这种模式展示了如何创建既通用又类型安全的 React hooks,适用于各种数据持久化场景。

泛型组件属性

创建能接受不同类型的通用组件:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>(props: ListProps<T>) {
  return (
    <ul>
      {props.items.map((item, index) => (
        <li key={index}>{props.renderItem(item)}</li>
      ))}
    </ul>
  );
}

泛型类组件

在类组件中使用泛型:

interface TableProps<T> {
  items: T[];
  columns: Array<{ key: keyof T; header: string }>;
}

class Table<T> extends React.Component<TableProps<T>> {
  render() {
    // 实现细节...
  }
}

6. 高级 Hooks 模式

元组返回类型

自定义钩子返回元组类型:

function useCounter(initialValue: number): [number, () => void, () => void] {
  const [count, setCount] = useState(initialValue);

  const increment = useCallback(() => setCount(c => c + 1), []);
  const decrement = useCallback(() => setCount(c => c - 1), []);

  return [count, increment, decrement];
}

必需的上下文

创建类型安全的 React 上下文:

interface UserContextType {
  user: User;
  setUser: (user: User) => void;
}

const UserContext = createContext<UserContextType | undefined>(undefined);

function useUserContext() {
  const context = useContext(UserContext);
  if (context === undefined) {
    throw new Error("useUserContext must be used within a UserProvider");
  }
  return context;
}

useState 中的联合类型

useState 中使用联合类型处理多种状态:

type Status = "idle" | "loading" | "success" | "error";

const [status, setStatus] = useState<Status>("idle");

函数重载

使用函数重载创建灵活的钩子 API:

function useFetch<TData>(url: string): { data: TData | null; loading: boolean };
function useFetch<TData, TError>(url: string, options: { errorHandler: true }): { data: TData | null; loading: boolean; error: TError | null };
function useFetch<TData, TError= unknown>(url: string, options?: { errorHandler?: boolean }) {
  // 实现细节...
}

7. TypeScript 类型深入探讨

React 命名空间导出

理解 React 命名空间中的类型:

// 例如:React.ReactNode, React.ComponentType, React.ElementType 等

JSX 元素类型

深入了解 JSX 元素的类型系统:

// JSX.Element vs React.ReactElement vs React.ReactNode

全局命名空间扩展

扩展全局类型定义:

declare global {
  interface Window {
    myCustomProperty: string;
  }
}

添加新的全局元素

向 JSX 添加自定义元素:

declare global {
  namespace JSX {
    interface IntrinsicElements {
      "custom-element": React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>;
    }
  }
}

8. 高级设计模式

懒加载组件

类型安全的懒加载组件:

const LazyComponent = React.lazy(() => import("./Component"));

function App() {
  return (
    <React.Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </React.Suspense>
  );
}

渲染属性模式

类型安全的渲染属性模式:

interface RenderProps {
  render: (value: number) => React.ReactNode;
}

function Counter({ render }: RenderProps) {
  const [count, setCount] = useState(0);
  return (
    <div>
      <button onClick={()=> setCount(count + 1)}>Increment</button>
      {render(count)}
    </div>
  );
}

转发引用

使用 forwardRef 并保持类型安全:

type ButtonProps = {
  variant: "primary" | "secondary";
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant, ...rest }, ref) => {
    return <button ref={ref} className={`btn-${variant}`} {...rest} />;
  }
);

高阶组件

类型安全的高阶组件:

function withLogging<T extends React.ComponentType<any>>(
  Component: T
): React.ComponentType<React.ComponentProps<T>> {
  const displayName = Component.displayName || Component.name || "Component";

  const WithLogging = (props: React.ComponentProps<T>) => {
    useEffect(() => {
      console.log(`Component ${displayName} rendered`);
    }, []);

    return <Component {...props} />;
  };

  WithLogging.displayName = `WithLogging(${displayName})`;

  return WithLogging;
}

as 属性模式

创建多态组件:

type BoxProps<E extends React.ElementType= "div"> = {
  as?: E;
} & React.ComponentPropsWithoutRef<E>;

function Box<E extends React.ElementType= "div">(
  { as, ...rest }: BoxProps<E>
) {
  const Component = as || "div";
  return <Component {...rest} />;
}

9. 与外部库集成

React Hook Form

类型安全地使用 React Hook Form:

interface FormValues {
  email: string;
  password: string;
}

function LoginForm() {
  const { register, handleSubmit } = useForm<FormValues>();

  const onSubmit = (data: FormValues) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("email")} />
      <input type="password" {...register("password")} />
      <button type="submit">Login</button>
    </form>
  );
}

React Select

类型安全地使用 React Select:

interface Option {
  value: string;
  label: string;
}

function SelectExample() {
  const options: Option[] = [
    { value: "chocolate", label: "Chocolate" },
    { value: "strawberry", label: "Strawberry" },
    { value: "vanilla", label: "Vanilla" }
  ];

  const [selectedOption, setSelectedOption] = useState<Option | null>(null);

  return (
    <Select<Option>
      value={selectedOption}
      onChange={setSelectedOption}
      options={options}
    />
  );
}

React Query

类型安全地使用 React Query:

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

function UserProfile({ userId }: { userId: number }) {
  const { data, isLoading, error } = useQuery<User, Error>(
    ["user", userId],
    () => fetchUser(userId)
  );

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return data ? <div>{data.name}</div> : null;
}