React 与 TypeScript
React + TypeScript 实战:Props 与事件类型、Hooks 泛型、Context 类型安全模式、React 19 类型系统变化(ref 即 prop、useRef 必传初始值、JSX 命名空间迁移)与常用类型速查。
前置知识
建议先阅读以下内容再进入本文:
1. 一句话理解
React 组件是”函数 + props 契约”,TypeScript 的作用就是把这份契约变成编译期可检查的:props 少传、传错、拼错字段名,写代码时就报错,而不是等到运行时白屏。心智模型很简单——把 props 当函数参数签名写,把事件处理器当 DOM 事件类型写,剩下的交给类型推断。新版 Vite 模板(pnpm create vite@latest my-app -- --template react-ts)开箱即含 @types/react,无需额外配置即可开始。
2. 基础配置:四个关键开关
// tsconfig.json 中与 React 相关的关键项
{
"compilerOptions": {
"jsx": "react-jsx", // 新 JSX 转换:无需 import React
"strict": true, // 严格模式全家桶(null 检查、noImplicitAny 等)
"moduleResolution": "bundler", // 打包器解析语义,Vite 项目标配
"skipLibCheck": true // 跳过第三方 .d.ts 内部检查,加快编译
}
}
strict: true 是底线。React 生态的类型质量整体很高,关掉严格模式等于主动放弃 80% 的收益。
3. Props 类型:从 interface 到可辨识联合
import { type ReactNode } from 'react';
interface ButtonProps {
variant?: 'primary' | 'ghost'; // 字面量联合:拼错 'primay' 直接编译报错
disabled?: boolean;
children?: ReactNode; // children 必须显式声明(FC 已不含隐式 children)
onClick?: () => void;
}
export function Button({ variant = 'primary', disabled, children, onClick }: ButtonProps) {
return (
<button className={variant} disabled={disabled} onClick={onClick}>
{children}
</button>
);
}
进阶利器是可辨识联合:用同一字段把互斥的 props 分组,调用处写错组合会被编译器指出:
type LinkProps =
| { kind: 'external'; href: string } // 外链必须给 href
| { kind: 'route'; to: string; replace?: boolean }; // 内跳必须给 to
function Nav(props: LinkProps) {
// props.kind === 'external' 的分支里只有 href,没有 to——TS 自动收窄
return props.kind === 'external'
? <a href={props.href} target="_blank">{props.href}</a>
: <RouteLink to={props.to} replace={props.replace} />;
}
不想手写长 props 时,用工具类型从现有组件或 DOM 元素”抄”:React.ComponentProps<typeof Button>(组件)、React.ComponentProps<'input'>(原生元素),详见文末速查。
4. 事件类型:只记四个
React 事件类型统一是 React.XxxEvent<目标元素类型>,日常只用到四个:
import { useState } from 'react';
export function Form() {
const [value, setValue] = useState('');
return (
<form
onSubmit={(e: React.FormEvent<HTMLFormElement>) => e.preventDefault()}
>
<input
value={value}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setValue(e.target.value)}
/>
<button onClick={(e: React.MouseEvent<HTMLButtonElement>) => e.stopPropagation()}>
提交
</button>
<input onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') submit(value);
}} />
</form>
);
}
省事技巧:内联在 JSX 上的处理器(如上)可以完全省略类型标注——onChange 的位置已让 TS 推断出 e 的类型。只有把处理器提取成独立函数时才必须标注。
5. Hooks 的类型:推断优先,显式兜底
import { useRef, useState } from 'react';
export function Counter() {
// useState 从初值推断出 number;setState((n) => n + 1) 也自动约束
const [count, setCount] = useState(0);
// 初值是 null/空对象等"不完整"值时,显式给泛型
const [user, setUser] = useState<{ id: number; name: string } | null>(null);
// useRef 三种形态(React 19 类型起必须传初始值):
const inputRef = useRef<HTMLInputElement>(null); // 挂 DOM:初值 null
const timerRef = useRef<number | undefined>(undefined); // 存可变量
const countRef = useRef(0); // 从初值推断 RefObject<number>(19 起可写)
return (
<div>
<span>{count}</span>
<input ref={inputRef} />
{/* user 可能为 null:strict 模式强制先收窄再使用 */}
{user && <p>{user.name}</p>}
<button onClick={() => setCount((n) => n + 1)}>+1</button>
</div>
);
}
React 19 类型系统的三个变化要记牢:useRef 必须传一个参数(无参调用编译报错);useRef(initial) 返回的 RefObject 的 current 是可写的(旧版 MutableRefObject 的职责并入其中,旧类型仍兼容但推荐直接用新形态);挂 DOM 的 ref 写 useRef<HTMLDivElement>(null),使用处 TS 会提示 current 可能为 null。
6. Context 的类型安全模式
Context 的问题是 createContext<T>(null) 会让消费者拿到 T | null,到处非空断言。标准解法是把 Provider 和 useXxxContext 一起封装,null 检查只做一次:
import { createContext, useContext, useState, type ReactNode } from 'react';
interface AuthCtx {
user: string | null;
login: (name: string) => void;
logout: () => void;
}
// 默认值给 null,但绝不导出这个 Context 本体
const AuthContext = createContext<AuthCtx | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<string | null>(null);
return (
<AuthContext.Provider
value={{ user, login: setUser, logout: () => setUser(null) }}
>
{children}
</AuthContext.Provider>
);
}
// 对外只暴露这个 Hook:出了 Provider 范围直接抛清晰错误
export function useAuth(): AuthCtx {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth 必须在 <AuthProvider> 内使用');
return ctx; // 此处之后类型收窄为 AuthCtx,null 消失
}
预期收益:组件里写 const { user } = useAuth() 时没有任何 | null 噪音;漏包 Provider 时报错信息指名道姓,而不是莫名其妙的 undefined 解构崩溃。
7. 泛型组件:一个组件适配多种数据
列表、表格、下拉这类”结构相同、数据类型不同”的组件适合写成泛型:
interface ListProps<T> {
items: T[];
getKey: (item: T) => string;
renderItem: (item: T) => React.ReactNode;
}
export function List<T>({ items, getKey, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={getKey(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// 使用处:T 自动推断为 { id: number; name: string }
<List
items={[{ id: 1, name: '钢笔' }, { id: 2, name: '笔记本' }]}
getKey={(item) => String(item.id)} // item 已知有 id 字段
renderItem={(item) => item.name}
/>;
推断失误(比如初始 items 传空数组导致 T = never)时,用 <List<MyItem> ... /> 显式标注即可。
8. React 19 类型变化清单
- ref 即 prop:函数组件可以直接接收
ref,绝大多数场景不再需要forwardRef;需要操作 ref 的子组件像写普通 prop 一样声明ref?: React.Ref<HTMLInputElement>。 - useRef 必须传参:
useRef()改为useRef(undefined)或带初值;错误在编译期暴露。 - JSX 命名空间迁移:全局
JSX.Element移入React.JSX,推荐统一写React.JSX.Element(或干脆用ReactNode声明渲染结果);自定义 JSX 命名空间的库需在declare module 'react'里扩展。 - FC 不含 children(18 起已生效):
React.FC<Props>不再隐式带children,需要时写PropsWithChildren<Props>。
9. 常见陷阱
- 事件参数标成
any或 DOM 原生事件:onChange(e: React.ChangeEvent<HTMLInputElement>)才对;标成原生Event会拿不到e.target.value的正确类型,标any则失去全部保护。 - 滥用
as断言:as SomeType是”闭嘴指令”不是转换;接口边界(API 响应、localStorage)之外出现成片as时应改写类型或加运行时校验。 - useState 存复杂对象后直接改字段:
user.name = 'x'不会触发更新且类型不报错;用setUser((u) => ({ ...u, name: 'x' }))。 - 样式对象写 CSS 属性名驼峰:
React.CSSProperties只认驼峰(backgroundColor);确需 CSS 变量用as React.CSSProperties(见速查),确需连字符属性用PropertiesHyphen。 - 给
ReactNode的 prop 传布尔用于条件渲染导致结构歧义:{flag && <X/>}里flag为 0 时会渲染出 “0”——ReactNode包含 number,与 TS 无关但常在 TS 项目里被忽略,条件渲染用三元或!!flag。 - 第三方库类型缺失就
any:优先看包是否带types字段或@types/*;都没有再写最小局部声明(declare module 'lib'),保持项目主体强类型。
10. 小结
初学者要点:
- Props 用 interface 声明、字面量联合约束枚举值、
children用ReactNode;内联事件处理器不用标类型。 useState尽量靠推断,初值不完整(null/空对象)时给显式泛型;挂 DOM 的 ref 写useRef<HTMLDivElement>(null)。- Context 封装成
Provider + useXxxContext,null 检查只做一次。
进阶注意:
- React 19 类型:ref 即 prop(弃用
forwardRef)、useRef必传参、JSX命名空间移到React.JSX、RefObject.current可变。 - 可辨识联合与泛型组件是把”能编译”提升到”类型即文档”的两件核心武器;工具类型(
ComponentProps/ReturnType/Awaited)能从现有代码反推类型,避免重复手写。
速查
ComponentProps 提取属性
ComponentProps
type <Props> = React.ComponentProps<<ElementType>>;
type DivProps = React.ComponentProps<'div'>;
type BtnProps = React.ComponentProps<'button'>;
type CompProps = React.ComponentProps<typeof MyComponent>;
ComponentPropsWithRef 含 ref
React.ComponentPropsWithRef<<ElementType>>
type InputProps = React.ComponentPropsWithRef<'input'>;
ComponentPropsWithoutRef 排除 ref
React.ComponentPropsWithoutRef<<ElementType>>
type PureProps = React.ComponentPropsWithoutRef<'div'>;
ReactNode 节点类型
ReactNode 任意节点
type <V> = React.ReactNode;
type Props = {
title: string;
children: React.ReactNode;
icon?: React.ReactNode;
};
ReactElement 单元素
React.ReactElement
const el: React.ReactElement = <div>hello</div>;
ReactElement 带泛型
React.ReactElement<<T>>
const el: React.ReactElement<{ value: string }> = <Comp value="x" />;
FC 函数组件类型
FC 基础(18+ 不含隐式 children)
const <Component>: React.FC<<Props>>
type Props = { title: string };
const Title: React.FC<Props> = ({ title }) => <h1>{title}</h1>;
FC 含 children
React.FC<React.PropsWithChildren<<Props>>>
const Card: React.FC<React.PropsWithChildren<{ title: string }>> = ({
title,
children,
}) => <section><h2>{title}</h2>{children}</section>;
ChangeEvent 事件类型
ChangeEvent 表单
React.ChangeEvent<<Element>>
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
MouseEvent 鼠标
React.MouseEvent<<Element>>
const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
};
KeyboardEvent 键盘
React.KeyboardEvent<<Element>>
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') submit();
};
FormEvent 表单提交
React.FormEvent<<FormElement>>
const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
EventHandler 处理器类型
type Change = React.ChangeEventHandler<HTMLInputElement>;
type Click = React.MouseEventHandler<HTMLButtonElement>;
type KeyDown = React.KeyboardEventHandler<HTMLInputElement>;
CSSProperties 样式类型
CSSProperties 内联样式(驼峰属性名)
React.CSSProperties
const style: React.CSSProperties = {
display: 'flex',
gap: 8,
color: '#333',
};
<div style={style} />;
自定义 CSS 变量
const style = {
'--brand': '#0066ff',
width: '100%',
} as React.CSSProperties;
PropertiesHyphen 长划线(需 csstype 的独立类型)
React.CSSProperties 只认驼峰属性名,连字符写法要用 csstype 的 PropertiesHyphen
import type { PropertiesHyphen } from 'csstype'; // csstype 是 React 类型属性的来源库
const style: PropertiesHyphen = {
'background-color': 'red', // 与 CSS 文件写法一致
'font-size': '14px',
};
Ref 类型(React 19 形态)
挂 DOM 的 ref(必须传初值 null)
useRef<<Element>>(null)
const inputRef = useRef<HTMLInputElement>(null); // current: HTMLInputElement | null
RefObject(19 起 current 可变)
React.RefObject<<T>>
const counterRef = useRef(0); // RefObject<number>,current 可写
counterRef.current += 1;
RefCallback 回调 ref
React.RefCallback<<Element>>
const callback: React.RefCallback<HTMLDivElement> = (el) => {
if (el) observe(el);
};
常用类型别名
Dispatch 派发器
React.Dispatch<<Action>>
const dispatch: React.Dispatch<Action> = useDispatch();
Reducer
React.Reducer<<State>, <Action>>
const reducer: React.Reducer<State, Action> = (state, action) => state;
Awaited 异步结果类型
type User = Awaited<ReturnType<typeof fetchUser>>;
JSX 命名空间类型(19 起在 React.JSX 下)
React.JSX.Element
const heading: React.JSX.Element = <h1>Title</h1>;
React.JSX.IntrinsicElements 内置元素
React.JSX.IntrinsicElements['<tag>']
const divProps: React.JSX.IntrinsicElements['div'] = { id: 'root', className: 'box' };
ElementRef 提取元素类型
React.ElementRef<<ElementType>>
type InputEl = React.ElementRef<'input'>; // HTMLInputElement