vault backup: 2026-04-29 22:28:21
This commit is contained in:
@@ -7,45 +7,55 @@ create time: 2026-04-29 22:09
|
||||
|
||||
## 概述
|
||||
|
||||
TypeScript 为 React 提供编译时类型检查和智能提示。本文档系统梳理 Props、State、Hook、Ref 等核心场景的类型定义方式,以及进阶的泛型推导模式。
|
||||
TypeScript 为 React 提供编译时类型检查和智能提示,将运行时错误提前到编码阶段。本文档从 Props、State、Hook、Ref 等核心场景切入,逐步深入到泛型推导、区分联合类型和 Schema 校验等进阶模式。
|
||||
|
||||
> [!question] 为什么 React + TS 值得投入?
|
||||
>
|
||||
> React 本身是纯 JS 库,但大型项目中「谁在改什么数据」「这个函数接收什么参数」往往成为协作瓶颈。TS 让你在写代码时就能获得精确的类型反馈,而不是等到测试环节才发现 `undefined is not a function`。
|
||||
|
||||
## Props 类型定义
|
||||
|
||||
### 基础方式
|
||||
|
||||
```tsx
|
||||
// 方式1:interface(推荐,可 extend)
|
||||
// ✅ 方式1:interface(推荐,可 extend)
|
||||
interface ButtonProps {
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
const Button = ({ label, onClick }: ButtonProps) => <button onClick={onClick}>{label}</button>;
|
||||
const Button = ({ label, onClick }: ButtonProps) => (
|
||||
<button onClick={onClick}>{label}</button>
|
||||
);
|
||||
|
||||
// 方式2:type alias
|
||||
// ✅ 方式2:type alias(适合联合类型)
|
||||
type ButtonProps = { label: string; onClick?: () => void };
|
||||
|
||||
// ⚠️ 避免:函数参数解构后不再标注
|
||||
// 这样会导致每个参数无法被单独推断
|
||||
function Component({ a, b }) { ... } // any!
|
||||
// ❌ 避免:解构后不标注类型
|
||||
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 的合成事件类型
|
||||
// ✅ 使用 React 的合成事件类型
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.target.value; // string | number | string[]
|
||||
e.target.value; // string | number | string[](取决于 input type)
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault(); // 阻止表单默认提交
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
console.log(e.button); // 鼠标按键
|
||||
console.log(e.button); // 0=左键, 1=中键, 2=右键
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
@@ -53,18 +63,26 @@ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
};
|
||||
```
|
||||
|
||||
> [!warning] 常见误区:syntheticEvent 的池化问题
|
||||
>
|
||||
> React 17 之前合成事件会被复用(池化),异步访问时可能已被清空。在 `setTimeout` 或 Promise 回调中,请先读取到局部变量:
|
||||
> ```tsx
|
||||
> const value = e.target.value; // 先取值
|
||||
> setTimeout(() => console.log(value), 100);
|
||||
> ```
|
||||
|
||||
### Children 类型
|
||||
|
||||
```tsx
|
||||
// 通用 children 类型
|
||||
// 通用 children —— 接受任意合法 React 节点
|
||||
function Card({ children }: { children: React.ReactNode }) {}
|
||||
|
||||
// 严格类型 children(限制允许的子节点类型)
|
||||
// 严格类型 children —— 限制允许的子节点类型
|
||||
interface TabsProps {
|
||||
children: React.ReactElement<TabProps>; // 只能是 Tab 组件
|
||||
}
|
||||
|
||||
// 数组形式
|
||||
// 泛型 children —— 子节点携带的数据类型可参数化
|
||||
interface ListProps<T> {
|
||||
children: React.ReactElement<{ item: T }> [];
|
||||
}
|
||||
@@ -72,22 +90,42 @@ interface ListProps<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
|
||||
user?.name; // ✅ 可选链,安全
|
||||
(user as User).name; // or: 类型断言
|
||||
user!?.name; // non-null assertion(慎用)
|
||||
```
|
||||
|
||||
// useReducer 类型推导
|
||||
> [!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 };
|
||||
type Action =
|
||||
| { type: "SET_FILTER"; payload: string }
|
||||
| { type: "ADD_ITEM"; payload: Item };
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
switch (action.type) {
|
||||
@@ -100,8 +138,10 @@ const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
## Ref 类型定义
|
||||
|
||||
### useRef
|
||||
|
||||
```tsx
|
||||
// 元素 ref
|
||||
// 元素 ref —— 最常用
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// mutable value ref(不触发 re-render)
|
||||
@@ -111,24 +151,35 @@ timerIdRef.current = setInterval(() => {}, 1000);
|
||||
// class component style ref object(用于挂载子组件引用)
|
||||
const childRef = useRef<ComponentInstance>(null);
|
||||
// 需要 useImperativeHandle 暴露方法给父级
|
||||
```
|
||||
|
||||
// forwardRef 中 ref 的正确用法
|
||||
const FancyInput = forwardRef<HTMLInputElement, { label: string }>(function FancyInput(props, ref) {
|
||||
const innerRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => innerRef.current?.focus(),
|
||||
blur: () => innerRef.current?.blur(),
|
||||
}));
|
||||
|
||||
return <input ref={innerRef} label={props.label} />;
|
||||
});
|
||||
> [!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
|
||||
// 自定义 Hook 返回值类型
|
||||
interface UseCountReturn {
|
||||
count: number;
|
||||
increment: () => void;
|
||||
@@ -143,8 +194,22 @@ function useCount(initial = 0): UseCountReturn {
|
||||
decrement: () => setCount(c => c - 1),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
// generic hook —— 最强大的类型推导场景
|
||||
> [!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
|
||||
@@ -152,7 +217,7 @@ function useAsync<T, Args extends any[]>(
|
||||
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);
|
||||
@@ -165,23 +230,27 @@ function useAsync<T, Args extends any[]>(
|
||||
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
|
||||
// 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
|
||||
// 类型守卫 —— React 状态管理中最常用的类型模式
|
||||
interface SuccessAction { type: "success"; data: User[] }
|
||||
interface ErrorAction { type: "error"; error: string }
|
||||
interface ErrorAction { type: "error"; error: string }
|
||||
interface LoadingAction { type: "loading" }
|
||||
|
||||
type Action = SuccessAction | ErrorAction | LoadingAction;
|
||||
@@ -200,14 +269,238 @@ function reducer(state: State, action: Action): State {
|
||||
}
|
||||
```
|
||||
|
||||
> [!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 / if"| B["Type Narrowing"]
|
||||
B --> C["discriminated union: type field"]
|
||||
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]]
|
||||
|
||||
Reference in New Issue
Block a user