typescript

TypeScript + React 集成

By AI-Writer 15 min read

TypeScript + React 集成

将 TypeScript 与 React 结合使用,可以实现组件级别的类型安全。本文涵盖 React 18 中 TypeScript 集成的核心场景与最佳实践。

函数组件类型

React.FC vs 函数声明

React 18 中推荐使用函数声明而非 React.FC

tsx
// ✅ 推荐:函数声明(显式 props 类型)
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary";
}

function Button({ label, onClick, variant = "primary" }: ButtonProps) {
  return (
    <button className={`btn btn-${variant}`} onClick={onClick}>
      {label}
    </button>
  );
}

// ❌ 不推荐:React.FC(隐式 children,有争议)
const ButtonFC: React.FC<ButtonProps> = ({ label, onClick, variant = "primary" }) => (
  <button onClick={onClick}>{label}</button>
);

为什么不用 React.FC React.FC 隐式包含 children,且在泛型组件中行为不够灵活。

children 类型

显式声明 children

tsx
interface CardProps {
  title: string;
  children: React.ReactNode;
}

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-body">{children}</div>
    </div>
  );
}

// children 可以是任意 React 可渲染的内容
<Card title="Welcome">
  <p>This is a paragraph</p>
  <button>Click me</button>
</Card>

常见的 children 类型

tsx
type ChildrenOnly = React.ReactNode;        // 任意可渲染内容
type TextOnly = string | number;            // 仅文本
type SingleElement = React.ReactElement;    // 单个 React 元素
type NoChildren = null;                      // 无 children

事件处理

常见事件类型

tsx
// 鼠标事件
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
  console.log(e.currentTarget.dataset.id);
}

// 表单事件
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
  e.preventDefault();
}

// 变化事件
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
  console.log(e.target.value);
}

// 键盘事件
function handleKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
  if (e.key === "Enter") {
    console.log("Submit!");
  }
}

// Focus 事件
function handleFocus(e: React.FocusEvent<HTMLInputElement>) {
  console.log(e.target.name);
}

表单组件泛型

tsx
interface InputProps {
  name: string;
  value: string;
  onChange: React.ChangeEvent<HTMLInputElement>;
  type?: "text" | "email" | "password";
}

function Input({ name, value, onChange, type = "text" }: InputProps) {
  return <input name={name} value={value} onChange={onChange} type={type} />;
}

// Select 组件
interface SelectProps<T extends string> {
  name: string;
  value: T;
  onChange: (value: T) => void;
  options: Array<{ label: string; value: T }>;
}

function Select<T extends string>({ name, value, onChange, options }: SelectProps<T>) {
  return (
    <select
      name={name}
      value={value}
      onChange={(e) => onChange(e.target.value as T)}
    >
      {options.map((opt) => (
        <option key={opt.value} value={opt.value}>
          {opt.label}
        </option>
      ))}
    </select>
  );
}

泛型组件

列表组件

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

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

// 使用
interface User {
  id: number;
  name: string;
}

const users: User[] = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" }
];

<List
  items={users}
  keyExtractor={(user) => String(user.id)}
  renderItem={(user) => <span>{user.name}</span>}
/>

更简洁的泛型组件写法

tsx
// 使用 as 语法
function Select<T>() {
  // ...
}

// 或者显式泛型参数
<Select<string>
  value="hello"
  onChange={(val) => console.log(val)}
/>

forwardRef

forwardRef 允许父组件访问子组件的 DOM 元素或组件实例:

tsx
interface InputRefProps {
  label: string;
  defaultValue?: string;
}

// 不使用 forwardRef:无法从外部聚焦
const InputWithLabel = ({ label, defaultValue }: InputRefProps) => (
  <div>
    <label>{label}</label>
    <input defaultValue={defaultValue} />
  </div>
);

// 使用 forwardRef:可以从外部获取 ref
interface InputRef {
  focus: () => void;
  blur: () => void;
}

const InputWithRef = forwardRef<InputRef, InputRefProps>(
  ({ label, defaultValue }, ref) => {
    const inputRef = useRef<HTMLInputElement>(null);

    useImperativeHandle(ref, () => ({
      focus: () => inputRef.current?.focus(),
      blur: () => inputRef.current?.blur()
    }));

    return (
      <div>
        <label>{label}</label>
        <input ref={inputRef} defaultValue={defaultValue} />
      </div>
    );
  }
);

InputWithRef.displayName = "InputWithRef";

// 使用
const inputRef = useRef<InputRef>(null);

function handleFocus() {
  inputRef.current?.focus();
}

useRef 的类型

tsx
// 用于 DOM 元素
const divRef = useRef<HTMLDivElement>(null);
// divRef.current 是 HTMLDivElement | null

// 用于可变值(不触发重新渲染)
const counterRef = useRef<number>(0);
// counterRef.current 是 number(初始值类型)

// 用于存储 previous 值
const prevCountRef = useRef<number>();
const prevCount = prevCountRef.current;
prevCountRef.current = count;

useState 与泛型

tsx
// 显式类型(当初始值无法推断时)
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(false);

// 联合类型的初始值
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");

// 复杂状态
const [filters, setFilters] = useState<{
  search: string;
  category: string | null;
  sortBy: "name" | "date";
}>({
  search: "",
  category: null,
  sortBy: "date"
});

Context 类型化

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

const ThemeContext = createContext<ThemeContextValue | null>(null);

function useTheme(): ThemeContextValue {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error("useTheme must be used within ThemeProvider");
  }
  return context;
}

// 使用
function ThemedButton() {
  const { theme, toggleTheme } = useTheme();
  return (
    <button className={theme} onClick={toggleTheme}>
      Toggle Theme
    </button>
  );
}

常用 React 类型速查

tsx
// 元素类型
type FC<P = {}> = React.FunctionComponent<P>;
type VFC<P = {}> = React.VoidFunctionComponent<P>; // 不使用 children
type Element = React.ReactElement;
type Node = React.ReactNode;
type Children = React.ReactNode;

// 事件类型
type MouseEvent = React.MouseEvent<Element>;
type ChangeEvent = React.ChangeEvent<Element>;
type FormEvent = React.FormEvent<Element>;
type KeyboardEvent = React.KeyboardEvent<Element>;

// DOM 元素类型
type HTMLElement = React.HTMLAttributes<Element>;
type ButtonElement = React.ButtonHTMLAttributes<HTMLButtonElement>;
type InputElement = React.InputHTMLAttributes<HTMLInputElement>;

总结

  • 组件类型:优先使用函数声明 + 显式 props 类型,而非 React.FC
  • childrenReact.ReactNode 是最通用的 children 类型
  • 事件类型:使用 React.MouseEvent<T>React.ChangeEvent<T> 等泛型类型
  • 泛型组件:为列表、分页等复用场景创建泛型组件
  • forwardRef:使用 forwardRef<RefType, PropsType> 获取 DOM 引用
  • useRef:泛型指定 HTMLDivElement | null 用于 DOM,指定 T 用于可变值
  • Context:为每个 Context 定义完整的接口类型
#typescript #react #react-18 #integration

评论

A

Written by

AI-Writer

Related Articles

typescript
#5

枚举与 const 断言

详细讲解 TypeScript 数字枚举、字符串枚举、枚举反向映射,以及 const 断言在对象和数组中的高级用法

Read More
typescript
#2

接口与类型别名

深入讲解 TypeScript 中接口的声明合并、可选属性、只读属性,以及类型别名与接口的选择策略

Read More