CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
TypeScript 核心/课时/React 组件 typing
RawBlame
课时目录12 节

TypeScript 核心

  1. 01TypeScript 是什么
  2. 02基本类型
  3. 03函数类型
  4. 04接口与类型别名
  5. 05联合与交叉类型
  6. 06类型收窄
  7. 07泛型
  8. 08工具类型
  9. 09模块与 tsconfig
  10. 10React 组件 typing
  11. 11异步与 API 类型
  12. 12综合练习与 Next.js

React 组件 typing

Props、事件、useState/useRef 泛型。

35 分钟

React 组件 typing

这一课你将能做到

  1. 给函数组件的 Props、State、事件、ref、children 写完整类型
  2. 写出泛型 React 组件,让组件在复用时保留类型信息
  3. 处理 React 开发中最常见的类型场景:受控组件、表单、ref 转发、Context

为什么要学

React 是 TypeScript 最大的应用场景。没有类型,React 组件的 props 就是黑盒——传错一个字段,页面不渲染,你只能靠肉眼在 JSX 里逐行排查。有了类型,编辑器自动补全 props、事件签名、ref 类型,传错一个字段当场红线。

这一课是「TS 理论 → React 实战」的最后一公里。前面学的接口、联合、泛型、工具类型,全部在这里汇合。写不好 React 组件类型,等于前面白学。

先建立图像

React 组件的类型关注点:

┌─────────────────────────────────────┐
│           React 组件                │
│                                     │
│  ┌─────────┐    ┌───────────────┐  │
│  │  Props   │───▶│  组件内部状态  │  │
│  │ (外部输入)│    │   useState    │  │
│  └─────────┘    └───────────────┘  │
│       │                            │
│       ▼                            │
│  ┌─────────┐    ┌───────────────┐  │
│  │ 事件处理 │◀───│   事件对象     │  │
│  │ onChange │    │  React.SyntheticEvent │
│  └─────────┘    └───────────────┘  │
│       │                            │
│       ▼                            │
│  ┌─────────┐                       │
│  │  Ref    │───▶ DOM 元素 / 子组件实例 │
│  └─────────┘                       │
└─────────────────────────────────────┘

核心原则:Props 用接口描述,事件用 React 内置类型,ref 用泛型指定目标元素,children 用 ReactNode。

跟着做

新建 react-demo.tsx,逐步实操。

步骤 1:最基础的组件 Props

interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: "primary" | "secondary" | "danger";
}

function Button({ label, onClick, disabled = false, variant = "primary" }: ButtonProps) {
  const baseStyle: React.CSSProperties = {
    padding: "8px 16px",
    border: "none",
    borderRadius: "4px",
    cursor: disabled ? "not-allowed" : "pointer",
    opacity: disabled ? 0.6 : 1,
  };

  const variantStyles: Record<string, React.CSSProperties> = {
    primary: { backgroundColor: "#007bff", color: "#fff" },
    secondary: { backgroundColor: "#6c757d", color: "#fff" },
    danger: { backgroundColor: "#dc3545", color: "#fff" },
  };

  return (
    <button
      style={{ ...baseStyle, ...variantStyles[variant] }}
      onClick={onClick}
      disabled={disabled}
    >
      {label}
    </button>
  );
}

// 使用
function App() {
  return (
    <div>
      <Button label="主要按钮" onClick={() => console.log("clicked")} />
      <Button label="危险按钮" variant="danger" onClick={() => {}} />
      {/* <Button label={123} onClick={() => {}} /> */}
      {/* 报错:label 必须是 string */}
    </div>
  );
}

步骤 2:State 类型

import { useState } from "react";

function UserProfile() {
  // 简单类型推断
  const [name, setName] = useState("");           // string
  const [age, setAge] = useState(0);               // number
  const [isEditing, setIsEditing] = useState(false); // boolean

  // 复杂类型要显式标注
  interface User {
    id: number;
    name: string;
  }
  const [user, setUser] = useState<User | null>(null); // 需要 null 的情况

  // 数组类型
  const [tags, setTags] = useState<string[]>([]);

  // 联合类型状态机
  type Status = "idle" | "loading" | "success" | "error";
  const [status, setStatus] = useState<Status>("idle");

  const handleLogin = () => {
    setStatus("loading");
    // 模拟 API
    setTimeout(() => {
      setUser({ id: 1, name: "小明" });
      setStatus("success");
    }, 500);
  };

  return (
    <div>
      <p>状态:{status}</p>
      {user && <p>用户:{user.name}</p>}
      <button onClick={handleLogin}>模拟登录</button>
    </div>
  );
}

步骤 3:事件类型

import { useState } from "react";

function Form() {
  const [value, setValue] = useState("");

  // input onChange 事件
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value); // e.target 是 HTMLInputElement,有 value 属性
  };

  // form onSubmit 事件
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log("提交:", value);
  };

  // button onClick 事件
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log("点击位置:", e.clientX, e.clientY);
  };

  // 键盘事件
  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === "Enter") {
      console.log("按了回车");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        value={value}
        onChange={handleChange}
        onKeyDown={handleKeyDown}
      />
      <button type="submit" onClick={handleClick}>提交</button>
    </form>
  );
}

常用事件类型速查:

事件类型
onChangeReact.ChangeEvent<T>
onClickReact.MouseEvent<T>
onSubmitReact.FormEvent<T>
onKeyDownReact.KeyboardEvent<T>
onFocusReact.FocusEvent<T>

T 是元素类型:HTMLInputElement、HTMLButtonElement、HTMLFormElement 等。

步骤 4:ref 类型

import { useRef, useEffect } from "react";

function VideoPlayer() {
  // 1. DOM ref:指定元素类型
  const videoRef = useRef<HTMLVideoElement>(null);
  const inputRef = useRef<HTMLInputElement>(null);

  // 2. 存储普通值(不触发重渲染)
  const timerRef = useRef<number | null>(null);

  useEffect(() => {
    // videoRef.current 被推断为 HTMLVideoElement | null
    if (videoRef.current) {
      videoRef.current.volume = 0.5;
    }
  }, []);

  const startTimer = () => {
    timerRef.current = window.setInterval(() => {
      console.log("tick");
    }, 1000);
  };

  const stopTimer = () => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
  };

  return (
    <div>
      <video ref={videoRef} src="/video.mp4" />
      <input ref={inputRef} />
      <button onClick={startTimer}>开始</button>
      <button onClick={stopTimer}>停止</button>
    </div>
  );
}

步骤 5:children 类型

import { ReactNode } from "react";

interface CardProps {
  title: string;
  children: ReactNode; // 任何可渲染的内容
}

function Card({ title, children }: CardProps) {
  return (
    <div style={{ border: "1px solid #ddd", borderRadius: "8px", padding: "16px" }}>
      <h3>{title}</h3>
      <div>{children}</div>
    </div>
  );
}

// children 可以是任何 JSX
function App2() {
  return (
    <Card title="用户信息">
      <p>姓名:小明</p>
      <Button label="编辑" onClick={() => {}} />
    </Card>
  );
}

// 更严格的 children:只允许特定组件
interface TabsProps {
  children: React.ReactElement<TabProps> | React.ReactElement<TabProps>[];
}

interface TabProps {
  label: string;
}

function Tab({ label }: TabProps) {
  return <div>{label}</div>;
}

function Tabs({ children }: TabsProps) {
  return <div>{children}</div>;
}

步骤 6:泛型组件

// 通用 Select 组件,选项类型由调用方决定
interface Option<T> {
  value: T;
  label: string;
}

interface SelectProps<T> {
  options: Option<T>[];
  value: T;
  onChange: (value: T) => void;
}

function Select<T>({ options, value, onChange }: SelectProps<T>) {
  return (
    <select
      value={String(value)}
      onChange={(e) => {
        const selected = options.find((o) => String(o.value) === e.target.value);
        if (selected) onChange(selected.value);
      }}
    >
      {options.map((opt) => (
        <option key={String(opt.value)} value={String(opt.value)}>
          {opt.label}
        </option>
      ))}
    </select>
  );
}

// 使用:数字选项
function App3() {
  const [size, setSize] = useState(16);

  return (
    <Select
      options={[
        { value: 12, label: "小号" },
        { value: 16, label: "中号" },
        { value: 20, label: "大号" },
      ]}
      value={size}
      onChange={setSize} // setSize 自动推断为 (value: number) => void
    />
  );
}

步骤 7:Context 类型

import { createContext, useContext, useState, ReactNode } from "react";

interface ThemeContextType {
  theme: "light" | "dark";
  toggle: () => void;
}

// 1. 创建 Context 时给类型和默认值
const ThemeContext = createContext<ThemeContextType>({
  theme: "light",
  toggle: () => {},
});

// 2. Provider 组件
function ThemeProvider({ children }: { children: ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("light");

  const toggle = () => setTheme((t) => (t === "light" ? "dark" : "light"));

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

// 3. 消费组件
function ThemedButton() {
  const { theme, toggle } = useContext(ThemeContext);

  return (
    <button
      style={{
        backgroundColor: theme === "light" ? "#fff" : "#333",
        color: theme === "light" ? "#333" : "#fff",
      }}
      onClick={toggle}
    >
      当前主题:{theme}
    </button>
  );
}

function App4() {
  return (
    <ThemeProvider>
      <ThemedButton />
    </ThemeProvider>
  );
}

讲清楚要点

  1. Props 用接口,State 用推断+注解:Props 是外部契约必须写清楚;State 简单类型让 TS 推断,复杂类型(对象/数组/联合)显式标注。
  2. 事件类型是 React 提供的泛型:React.ChangeEvent<HTMLInputElement> 的泛型参数是 DOM 元素类型,不是值类型。写错元素类型会导致 e.target 没有对应属性。
  3. ref 的两种用途:DOM 引用(useRef<HTMLInputElement>(null))和值存储(useRef<number>(0)),前者不触发重渲染,后者同样不触发。
  4. children 用 ReactNode:它是所有可渲染内容的联合(string、number、JSX、数组、null、undefined)。如果要限制 children 类型,用 ReactElement 或更精确的类型。
  5. 泛型组件的关键:<T,>(.tsx 文件)或 <T extends unknown>,确保编译器不把 <T> 解析成 JSX。
  6. Context 类型三步走:createContext<T>(defaultValue) → Provider value={...} → useContext 消费。默认值必须匹配类型,否则编译报错。

你可能会懵的地方

  • 为什么 useState 有时要显式写类型? 当初始值不能代表完整类型时:useState(null) 推断为 null,但你后续要赋 User 对象。useState<User | null>(null) 让类型覆盖完整。

  • e.target 和 e.currentTarget 有什么区别? target 是触发事件的元素(可能是子元素),currentTarget 是绑定事件的元素。input 上两者通常相同;嵌套元素点击时不同。类型上 currentTarget 更准确。

  • FC(FunctionComponent)类型要不要写? 不需要。React.FC 是历史遗留写法,现在直接写函数参数类型更简洁。FC 还会自动包含 children,有时反而多余。

  • 泛型组件在 JSX 里怎么写? <Select<number> options={...} />,但如果 options 里已经能推断出 number,可以省略 <number> 让 TS 自动推断。

  • forwardRef 的类型怎么写? forwardRef<HTMLInputElement, Props>((props, ref) => ...),两个泛型参数:ref 目标类型、Props 类型。

错法对照

错法问题正解
Props 用 any失去类型保护定义接口精确描述每个字段
useState(null) 后续赋对象类型推断为 null,赋值报错useState<T | null>(null)
事件 handler 参数不写类型隐式 any,strict 下报错写 React.ChangeEvent<HTMLInputElement> 等
ref 不给泛型 useRef(null)current 推断为 null,无法调用方法useRef<HTMLInputElement>(null)
children 写 any 或 JSX.Element太宽或太窄(不允许 string)用 ReactNode

卡点急救

  1. Property 'value' does not exist on type 'EventTarget':事件泛型参数错了。ChangeEvent<HTMLInputElement> 才有 value,HTMLButtonElement 没有。
  2. ref 报 Object is possibly 'null':useRef 初始值是 null,访问 .current 前判空或用 ?.。
  3. 泛型组件报 JSX element type does not have any construct signatures:<T> 被解析成 JSX。改成 <T,> 或 <T extends unknown>。
  4. Context 默认值类型不匹配:createContext 的默认值必须完整匹配类型,或给类型加 | null 并在消费时判空。

面试常问

  • React 组件 Props 类型怎么写? 定义接口描述每个字段的类型、可选性、函数签名。配合 ReactNode 处理 children,React.CSSProperties 处理 style。

  • useState 的类型怎么标注? 简单类型靠推断:useState("") → string。复杂类型显式标注:useState<User | null>(null)、useState<string[]>([])。

  • React 事件类型有哪些? ChangeEvent(onChange)、MouseEvent(onClick)、FormEvent(onSubmit)、KeyboardEvent(onKeyDown)、FocusEvent(onFocus)。泛型参数是 DOM 元素类型。

  • forwardRef 的类型怎么写? forwardRef<RefType, PropsType>((props, ref) => ...)。ref 参数是 ForwardedRef<RefType>,实际上是 RefObject<T> | ((instance: T | null) => void) | null 的联合。

自测三问

  1. React.ChangeEvent<HTMLInputElement> 和 React.ChangeEvent<HTMLButtonElement> 的 e.target 有什么区别?
  2. 为什么 useRef<HTMLVideoElement>(null) 访问 .current 时要判空,而 useRef<HTMLVideoElement>(document.createElement("video")) 不用?
  3. 写一个泛型组件 List<T>,接收 items: T[] 和 renderItem: (item: T) => ReactNode,并说明 <T,> 中逗号的作用。

小结一句话

React + TS 的核心是把 JSX 的「动态黑盒」变成「类型契约」:Props 接口化、事件泛型化、ref 精确化、children 用 ReactNode、复杂状态用联合——组件的每个输入输出都有编译器站岗。

延伸

  • React TypeScript Cheatsheet
  • 下一课:异步与 API 类型——Promise<T>、async/await、API 响应建模、zod 运行时校验。
加载本节练习…
← 上一课模块与 tsconfig下一课 →异步与 API 类型