Files
cs-note/hhs/REACT/3. 生态工具篇/10-TS + React.md
T
2026-05-24 11:42:38 +08:00

507 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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) => (
<button onClick={onClick}>{label}</button>
);
// ✅ 方式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<K,V>`) 时
### 合成事件类型
```tsx
// ❌ 不要用 HTML 原生的 Event
const handleChange = (e: Event) => {};
// ✅ 使用 React 的合成事件类型
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.target.value; // string | number | string[](取决于 input type)
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // 阻止表单默认提交
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.button); // 0=左键, 1=中键, 2=右键
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
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<TabProps>; // 只能是 Tab 组件
}
// 泛型 children —— 子节点携带的数据类型可参数化
interface ListProps<T> {
children: React.ReactElement<{ item: T }> [];
}
```
## State 类型推导
### useState
```tsx
interface User { name: string; role: "admin" | "user"; age: number }
// 完整泛型
const [user, setUser] = useState<User>({ name: "", role: "user", age: 0 });
// 可选初始值 —— 必须显式声明联合类型
const [user, setUser] = useState<User | null>(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<HTMLInputElement>(null);
// mutable value ref(不触发 re-render)
const timerIdRef = useRef<ReturnType<typeof setInterval>>(undefined);
timerIdRef.current = setInterval(() => {}, 1000);
// class component style ref object(用于挂载子组件引用)
const childRef = useRef<ComponentInstance>(null);
// 需要 useImperativeHandle 暴露方法给父级
```
> [!example] 为什么 ref.current 改变不会触发渲染?
>
> React 的更新机制只响应 setState 调用。ref 的设计初衷就是绕过 React 的响应式系统——比如在 useEffect 中保存上一次 props 的值、存储定时器 ID、或者调用子组件的原生 DOM API。如果你发现修改 ref 后需要 UI 同步更新,说明你可能应该用 state。
### forwardRef + useImperativeHandle
```tsx
const FancyInput = forwardRef<HTMLInputElement, { label: string }>(
function FancyInput(props, ref) {
const innerRef = useRef<HTMLInputElement>(null);
// 只暴露 focus 和 blur 给父组件,隐藏其他原生方法
useImperativeHandle(ref, () => ({
focus: () => innerRef.current?.focus(),
blur: () => innerRef.current?.blur(),
}));
return <input ref={innerRef} label={props.label} />;
}
);
```
## 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<T, Args extends any[]>(
asyncFn: (...args: Args) => Promise<T>,
deps: DependencyList
): { data: T | null; loading: boolean; error: Error | null; invoke: (...args: Args) => void } {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(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<ThemeContextType | null>(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 (
<ThemeContext.Provider value={{ theme, toggleTheme: () => setTheme(t => t === "light" ? "dark" : "light") }}>
{children}
</ThemeContext.Provider>
);
}
```
> [!tip] 多 Context 时的组合策略
>
> Context 数量增多后,推荐使用 **多个小型 Context** 而非一个巨型 Context:每个 Context 负责一小块职责(认证、主题、语言),这样消费者只会在自己关注的 context 变化时重渲染。也可以用一个 Context 包裹另一个,形成嵌套结构。
## 泛型组件与 Polymorphic Components
### 泛型列表组件
```tsx
interface SelectableTableProps<T> {
data: T[];
renderRow: (item: T, selected: boolean) => React.ReactNode;
selectedIds: Set<string>;
onSelect: (id: string) => void;
getId: (item: T) => string;
}
function SelectableTable<T>({
data, renderRow, selectedIds, onSelect, getId,
}: SelectableTableProps<T>) {
return (
<table>
<tbody>
{data.map(item => {
const id = getId(item);
const selected = selectedIds.has(id);
return <tr key={id} onClick={() => onSelect(id)}>
<td>{renderRow(item, selected)}</td>
</tr>;
})}
</tbody>
</table>
);
}
// 使用:T 自动推导为 User
<SelectableTable
data={users}
getId={(u: User) => u.id}
renderRow={(u, sel) => <span style={{ background: sel ? "#eee" : "" }}>{u.name}</span>}
selectedIds={selected}
onSelect={setId}
/>
```
### HTML 标签聚合组件(Polymorphic)
```tsx
// 基于 JSX.IntrinsicElements 实现 polymorphic component
type PolymorphicComponent<P, D extends keyof JSX.IntrinsicElements> = {
<As extends keyof JSX.IntrinsicElements = D>(
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 }) => (
<Tag className={`btn btn-${variant}`} {...props} />
);
```
> [!warning] Polymorphic Component 的类型难点
>
> 上述写法是简化版。生产环境通常借助第三方库如 `@radix-ui/react-slot` 来处理复杂的泛型推导,因为要让 `as="a"` 时同时合并 `<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<typeof LoginSchema>;
// 2. React Hook Form + ZodResolver 无缝对接
const { register, handleSubmit, formState: { errors } } = useForm<LoginFormValue>({
resolver: zodResolver(LoginSchema),
});
// 3. 模板中使用 —— errors 和 register 都有完整类型提示
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} placeholder="Email" />
{/* errors.email?.message 有类型提示 */}
{errors.email && <span>{errors.email.message}</span>}
<input {...register("password")} type="password" />
<button type="submit">登录</button>
</form>
```
> [!tip] 为什么选择 Zod 而非 Yup?
>
> - Zod 用 TypeScript 原生语法定义,无需额外 import 类型(Yup 需要 `yupToZooooootTypes` 等桥接方案)
> - Zod 零依赖、性能更优,且支持 `.parse()` 运行时校验与 `.infer()` 类型推导一体化
> - Zod 的错误消息更可定制化
## 常见坑点与避坑指南
> [!failure] 坑1:`any` 的隐式传播
>
> `Array<any>`、`Record<string, any>` 会让整个链条失去类型保护。替代方案:
> - 用 `unknown` 替代顶层 `any`(必须先类型守卫才能使用)
> - 用泛型 `<T>` 传递具体的数据结构
>
> ```tsx
> // ❌ 危险
> const data: Record<string, any> = {};
> data.foo.bar.baz; // 编译通过,运行崩溃
>
> // ✅ 安全
> const data: Record<string, unknown> = {};
> if (typeof data.foo === "object" && data.foo !== null) {
> data.foo.bar; // 需要额外的守卫
> }
> ```
> [!failure] 坑2:事件处理器中的类型收窄失效
>
> 箭头函数无法正确收窄:
> ```tsx
> // ❌ TS 无法推断 e 具体是哪个事件
> <button onClick={(e) => handleClick(e)}>Click</button>
>
> // ✅ 用命名函数保持类型信息
> <button onClick={(e) => handleClick(e)}>Click</button>
> // 但最好直接在属性上写,不包箭头
> <button onClick={handleClick}>Click</button>
> ```
> [!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<string[]>([]);
>
> // ✅ 或者用 non-empty array 初始化
> const [items, setItems] = useState(["default"]);
> ```
> [!failure] 坑4:React.FC 的副作用
>
> `React.FC` 在早期被广泛推荐,但现在社区趋于弃用原因如下:
> - 它会隐式包含 `children` prop(即使你的组件不需要)
> - 不支持泛型
> - 使得 ReturnType 无法正确推导
> - 使 HOC 的 WrappedComponent 类型丢失
>
> ```tsx
> // ❌ 不推荐
> const MyComponent: React.FC<MyProps> = ({ children }) => <div>{children}</div>;
>
> // ✅ 推荐
> function MyComponent({ children }: MyProps) {
> return <div>{children}</div>;
> }
> ```
## 关联笔记
- [[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]]