--- tags: [React, TypeScript, Frontend] create time: 2026-04-29 22:09 --- # TS + React ## 概述 TypeScript 为 React 提供编译时类型检查和智能提示,将运行时错误提前到编码阶段。本文档从 Props、State、Hook、Ref 等核心场景切入,逐步深入到泛型推导、区分联合类型和 Schema 校验等进阶模式。 > [!question] 为什么 React + TS 值得投入? > > React 本身是纯 JS 库,但大型项目中「谁在改什么数据」「这个函数接收什么参数」往往成为协作瓶颈。TS 让你在写代码时就能获得精确的类型反馈,而不是等到测试环节才发现 `undefined is not a function`。 ## Props 类型定义 ### 基础方式 ```tsx // ✅ 方式1:interface(推荐,可 extend) interface ButtonProps { label: string; onClick?: () => void; } const Button = ({ label, onClick }: ButtonProps) => ( ); // ✅ 方式2:type alias(适合联合类型) type ButtonProps = { label: string; onClick?: () => void }; // ❌ 避免:解构后不标注类型 function BadComponent({ a, b }) { ... } // a 和 b 都是 any! ``` > [!tip] interface vs type —— 何时用哪个? > > - **优先 interface**:它支持 `extends` / `implements`,更适合组件 Props 的层级扩展 > - **选 type**:当你需要联合类型 (`A | B`)、交叉类型 (`A & B`) 或映射类型 (`Record`) 时 ### 合成事件类型 ```tsx // ❌ 不要用 HTML 原生的 Event const handleChange = (e: Event) => {}; // ✅ 使用 React 的合成事件类型 const handleChange = (e: React.ChangeEvent) => { e.target.value; // string | number | string[](取决于 input type) }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // 阻止表单默认提交 }; const handleClick = (e: React.MouseEvent) => { console.log(e.button); // 0=左键, 1=中键, 2=右键 }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter") submit(); }; ``` > [!warning] 常见误区:syntheticEvent 的池化问题 > > React 17 之前合成事件会被复用(池化),异步访问时可能已被清空。在 `setTimeout` 或 Promise 回调中,请先读取到局部变量: > ```tsx > const value = e.target.value; // 先取值 > setTimeout(() => console.log(value), 100); > ``` ### Children 类型 ```tsx // 通用 children —— 接受任意合法 React 节点 function Card({ children }: { children: React.ReactNode }) {} // 严格类型 children —— 限制允许的子节点类型 interface TabsProps { children: React.ReactElement; // 只能是 Tab 组件 } // 泛型 children —— 子节点携带的数据类型可参数化 interface ListProps { children: React.ReactElement<{ item: T }> []; } ``` ## State 类型推导 ### useState ```tsx interface User { name: string; role: "admin" | "user"; age: number } // 完整泛型 const [user, setUser] = useState({ name: "", role: "user", age: 0 }); // 可选初始值 —— 必须显式声明联合类型 const [user, setUser] = useState(null); // 使用时需判空 user?.name; // ✅ 可选链,安全 (user as User).name; // or: 类型断言 user!?.name; // non-null assertion(慎用) ``` > [!tip] 利用构造函数推断 > > 当 initialState 是个对象字面量时,可以用类构造函数的模式让 TS 自动推导出 State 类型,减少重复书写: > ```tsx > class UserStore { > name = ""; > role: "admin" | "user" = "user"; > age = 0; > } > const [user, setUser] = useState(new UserStore()); > // 注意:这里 user 类型就是 new UserStore() 的实例类型 > ``` ### useReducer ```tsx interface State { items: Item[]; filter: string } type Action = | { type: "SET_FILTER"; payload: string } | { type: "ADD_ITEM"; payload: Item }; function reducer(state: State, action: Action): State { switch (action.type) { case "SET_FILTER": return { ...state, filter: action.payload }; case "ADD_ITEM": return { ...state, items: [...state.items, action.payload] }; } } const [state, dispatch] = useReducer(reducer, initialState); ``` ## Ref 类型定义 ### useRef ```tsx // 元素 ref —— 最常用 const inputRef = useRef(null); // mutable value ref(不触发 re-render) const timerIdRef = useRef>(undefined); timerIdRef.current = setInterval(() => {}, 1000); // class component style ref object(用于挂载子组件引用) const childRef = useRef(null); // 需要 useImperativeHandle 暴露方法给父级 ``` > [!example] 为什么 ref.current 改变不会触发渲染? > > React 的更新机制只响应 setState 调用。ref 的设计初衷就是绕过 React 的响应式系统——比如在 useEffect 中保存上一次 props 的值、存储定时器 ID、或者调用子组件的原生 DOM API。如果你发现修改 ref 后需要 UI 同步更新,说明你可能应该用 state。 ### forwardRef + useImperativeHandle ```tsx const FancyInput = forwardRef( function FancyInput(props, ref) { const innerRef = useRef(null); // 只暴露 focus 和 blur 给父组件,隐藏其他原生方法 useImperativeHandle(ref, () => ({ focus: () => innerRef.current?.focus(), blur: () => innerRef.current?.blur(), })); return ; } ); ``` ## Hook 类型推导 ### 自定义 Hook 返回值 ```tsx interface UseCountReturn { count: number; increment: () => void; decrement: () => void; } function useCount(initial = 0): UseCountReturn { const [count, setCount] = useState(initial); return { count, increment: () => setCount(c => c + 1), decrement: () => setCount(c => c - 1), }; } ``` > [!tip] 让返回值类型自动推导 > > 如果不想手动编写返回类型接口,可以借助 TypeScript 4.7+ 的 `as const` 技巧或使用 `ReturnType` 工具类型: > ```tsx > function useMouse() { > const [pos, setPos] = useState({ x: 0, y: 0 }); > useEffect(() => { /* ... */ }, []); > return pos; // TS 会自动推导出 { readonly x: number; readonly y: number } > } > ``` ### 泛型 Hook —— 最强大的类型推导场景 ```tsx function useAsync( asyncFn: (...args: Args) => Promise, deps: DependencyList ): { data: T | null; loading: boolean; error: Error | null; invoke: (...args: Args) => void } { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const invoke = useCallback(async (...args: Args) => { setLoading(true); setError(null); try { const result = await asyncFn(...args); setData(result); } catch (err) { setError(err as Error); } finally { setLoading(false); } }, [asyncFn, ...deps]); useEffect(() => { invoke(); }, [invoke]); return { data, loading, error, invoke }; } // T 自动推导!无需手动指定 const { data: users } = useAsync(fetchUsers, []); // data: User[] | null const { data: user } = useAsync(fetchUserById, [id]); // data: User | null const { data: config } = useAsync(fetchConfig, [env]); // data: Config | null ``` > [!question] 为什么 `Args extends any[]`? > > 这让我们可以同时参数化「异步函数的返回值类型」和「异步函数的入参」。例如 `fetchUserById(id: string)` 的 `T = User`,`Args = [string]`,类型信息从内到外全自动推导,不需要在调用处再次声明。 ## discriminated Union(区分联合类型) ```tsx interface SuccessAction { type: "success"; data: User[] } interface ErrorAction { type: "error"; error: string } interface LoadingAction { type: "loading" } type Action = SuccessAction | ErrorAction | LoadingAction; function reducer(state: State, action: Action): State { switch (action.type) { case "success": return { ...state, data: action.data, status: "loaded" }; // ✅ TS 自动推断 action.data 是 User[] case "error": return { ...state, error: action.error }; // ✅ TS 自动推断 action.error 是 string case "loading": return { ...state, status: "loading" }; } } ``` > [!important] exhaustive check 防漏分支 > > 添加一个 `default` 分支并用 `never` 类型确保所有联合成员都被覆盖: > ```tsx > function reducer(state: State, action: Action): State { > switch (action.type) { > case "success": return { ...state, status: "loaded" }; > case "error": return { ...state, status: "failed" }; > case "loading": return { ...state, status: "pending" }; > default: > const _exhaustiveCheck: never = action; > throw new Error(`Unhandled action type: ${_exhaustiveCheck}`); > } > } > ``` > 一旦新增了一个 Action 类型但没有在 switch 中处理,TS 编译器会立刻报错。 ```mermaid graph LR A["Union Type"] -->|"switch on type field"| B["Type Narrowing"] B --> C["discriminated union — 最优解"] B --> D["typeof check"] B --> E["in operator"] B --> F["instanceof check"] style C fill:#61DAFB,color:#000 style A fill:#fff,color:#000 ``` ## Context 类型定义 ```tsx interface ThemeContextType { theme: "light" | "dark"; toggleTheme: () => void; } // 创建 context 时传入默认值(可为 null,配合非空断言使用) const ThemeContext = createContext(null); // 封装类型安全的消费 Hook —— 比直接 useContext 更安全 function useTheme(): ThemeContextType { const ctx = useContext(ThemeContext); if (!ctx) throw new Error("useTheme must be used within ThemeProvider"); return ctx; } // Provider 类型 —— 将 value 的类型与 Context 绑定 function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setTheme] = useState<"light" | "dark">("light"); return ( setTheme(t => t === "light" ? "dark" : "light") }}> {children} ); } ``` > [!tip] 多 Context 时的组合策略 > > Context 数量增多后,推荐使用 **多个小型 Context** 而非一个巨型 Context:每个 Context 负责一小块职责(认证、主题、语言),这样消费者只会在自己关注的 context 变化时重渲染。也可以用一个 Context 包裹另一个,形成嵌套结构。 ## 泛型组件与 Polymorphic Components ### 泛型列表组件 ```tsx interface SelectableTableProps { data: T[]; renderRow: (item: T, selected: boolean) => React.ReactNode; selectedIds: Set; onSelect: (id: string) => void; getId: (item: T) => string; } function SelectableTable({ data, renderRow, selectedIds, onSelect, getId, }: SelectableTableProps) { return ( {data.map(item => { const id = getId(item); const selected = selectedIds.has(id); return onSelect(id)}> ; })}
{renderRow(item, selected)}
); } // 使用:T 自动推导为 User u.id} renderRow={(u, sel) => {u.name}} selectedIds={selected} onSelect={setId} /> ``` ### HTML 标签聚合组件(Polymorphic) ```tsx // 基于 JSX.IntrinsicElements 实现 polymorphic component type PolymorphicComponent = { ( props: P & { as?: As } & JSX.IntrinsicElements[As] ): ReactNode; }; // Button 可以是 button/div/a 等任何 HTML 元素 const Button: PolymorphicComponent< { variant?: "primary" | "secondary" }, "button" > = ({ variant = "primary", as: Tag = "button", ...props }) => ( ); ``` > [!warning] Polymorphic Component 的类型难点 > > 上述写法是简化版。生产环境通常借助第三方库如 `@radix-ui/react-slot` 来处理复杂的泛型推导,因为要让 `as="a"` 时同时合并 `` 的属性(`href`, `target` 等)而不产生冲突,泛型约束较为复杂。 ## Form 校验与 Zod Schema 现代 React + TS 项目中,推荐使用 **Zod** 等 Schema 库将校验逻辑和类型推导合二为一: ```tsx import { z } from "zod"; // 1. 定义 Schema —— 类型从 schema 自动推导 const LoginSchema = z.object({ email: z.string().email(), password: z.string().min(8), }); type LoginFormValue = z.infer; // 2. React Hook Form + ZodResolver 无缝对接 const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(LoginSchema), }); // 3. 模板中使用 —— errors 和 register 都有完整类型提示
{/* errors.email?.message 有类型提示 */} {errors.email && {errors.email.message}}
``` > [!tip] 为什么选择 Zod 而非 Yup? > > - Zod 用 TypeScript 原生语法定义,无需额外 import 类型(Yup 需要 `yupToZooooootTypes` 等桥接方案) > - Zod 零依赖、性能更优,且支持 `.parse()` 运行时校验与 `.infer()` 类型推导一体化 > - Zod 的错误消息更可定制化 ## 常见坑点与避坑指南 > [!failure] 坑1:`any` 的隐式传播 > > `Array`、`Record` 会让整个链条失去类型保护。替代方案: > - 用 `unknown` 替代顶层 `any`(必须先类型守卫才能使用) > - 用泛型 `` 传递具体的数据结构 > > ```tsx > // ❌ 危险 > const data: Record = {}; > data.foo.bar.baz; // 编译通过,运行崩溃 > > // ✅ 安全 > const data: Record = {}; > if (typeof data.foo === "object" && data.foo !== null) { > data.foo.bar; // 需要额外的守卫 > } > ``` > [!failure] 坑2:事件处理器中的类型收窄失效 > > 箭头函数无法正确收窄: > ```tsx > // ❌ TS 无法推断 e 具体是哪个事件 > > > // ✅ 用命名函数保持类型信息 > > // 但最好直接在属性上写,不包箭头 > > ``` > [!failure] 坑3:useState 初始值的类型陷阱 > > ```tsx > // ❌ 类型变成 never[] —— TS 推断了最窄的空数组类型 > const [items, setItems] = useState([]); > items.push("hello"); // error: Property 'push' does not exist on type 'never[]' > > // ✅ 显式声明泛型 > const [items, setItems] = useState([]); > > // ✅ 或者用 non-empty array 初始化 > const [items, setItems] = useState(["default"]); > ``` > [!failure] 坑4:React.FC 的副作用 > > `React.FC` 在早期被广泛推荐,但现在社区趋于弃用原因如下: > - 它会隐式包含 `children` prop(即使你的组件不需要) > - 不支持泛型 > - 使得 ReturnType 无法正确推导 > - 使 HOC 的 WrappedComponent 类型丢失 > > ```tsx > // ❌ 不推荐 > const MyComponent: React.FC = ({ children }) =>
{children}
; > > // ✅ 推荐 > function MyComponent({ children }: MyProps) { > return
{children}
; > } > ``` ## 关联笔记 - [[hhs/REACT/README.md]] - [[hhs/REACT/1. 基础篇/03-组件与 Props.md]] - [[hhs/REACT/1. 基础篇/04-State 与不可变性.md]] - [[hhs/REACT/3. 生态工具篇/08-路由管理.md]] - [[hhs/REACT/3. 生态工具篇/09-状态管理.md]]